New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

java.lang.ClassNotFoundException: Didn't find class "android.widget.fragment" #194

Closed
radzio opened this Issue Dec 2, 2014 · 12 comments

Comments

Projects
None yet
3 participants
@radzio

radzio commented Dec 2, 2014

When I've added fragment to my Activity layout xml using:

I got error:

Caused by: java.lang.RuntimeException: java.lang.ClassNotFoundException: Didn't find class "android.widget.fragment" on path: DexPathList[[zip file "/data/app/net.droidlabs.myapp-1.apk"],nativeLibraryDirectories=[/data/app-lib/net.droidlabs.myapp-1, /system/lib]]
            at org.robobinding.ViewFactory.onCreateView(ViewFactory.java:38)
            at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:685)

Is it possible to use fragments that way with RoboBinding?

minSdkVersion = 14
targetSdkVersion = 21
robobindingVersion = '0.8.10-SNAPSHOT'

@weicheng113

This comment has been minimized.

Member

weicheng113 commented Dec 3, 2014

Hi Radzio,

View name resolving source code is here - https://github.com/RoboBinding/RoboBinding/blob/develop/framework/src/main/java/org/robobinding/ViewNameResolver.java .

Custom fragments are a bit different from the normal custom UI components. So your custom fragments are specified in android:name tag. Would you like to have a try to resolve the issue and add unit tests for the scenario? It is quite straightforward. Sorry, I am quite busy lately.

Best regards,
Cheng Wei

@radzio

This comment has been minimized.

radzio commented Dec 11, 2014

I've tried to resolve this issue but it is harder than I've thought. I am not sure but probably it is needed to implement custom FragmentManager :(, or maybe it will be enough to return null in RoboBinding ViewFactory and have default behaviour... Do you have any ideas which direction is better ;-)?

@weicheng113

This comment has been minimized.

Member

weicheng113 commented Dec 12, 2014

Hi Radek,

I have no idea at the moment. I will need to experiment before getting a
suitable solution. Can u extract the view class from android:name?

Cheers,
Cheng

I've tried to resolve this issue but it is harder than I've thought. I am
not sure but probably it is needed to implement custom FragmentManager :(,
or maybe it will be enough to return null in RoboBinding ViewFactory and
have default behaviour... Do you have any ideas which direction is better
;-)?


Reply to this email directly or view it on GitHub
#194 (comment)
.

@vangorra

This comment has been minimized.

vangorra commented Feb 3, 2015

While I do not have a use for robobinding, I stumbled across this thread while searching for a solution to my own problems. While my solution won't directly fit into Robobinding's codebase but hopefully this will help inspire a solution.
At work, I have created a library similar to robobinding. I call MyInflater.with(content).inflate(...); in order to inflate layouts and bind to special attributes. There is a bunch of proprietary stuff that happens within, but you get the idea. The problem is, the view inflation of android is very delicate and breaks easily. I can empathize with weicheng113.

I tried creating custom combinations of custom ContextWrapper, LayoutInflater and LayoutInflater.Factory. The results were always some obscure error about context (stale, old or wrong) or crashes when attempting to inflate special tags like "fragment". Using custom ContextWrapper and LayoutInflater has complications in that you run the risk of breaking compatibility with third-party libraries like Calligraphy.

Ultimately, I was able to get things working by using only a custom LayoutInflater.Factory. Since I expect my static class to be called for all inflate actions, I ended up cloning the LayoutInflater and setting the factory on the new inflater. Keep in mind, my solution was a combination of the the Factory and how I handled the LayoutInflater/Context. Basically, I kept the Context/Inflater completely unadulterated, clone the inflater and set the factory.
Keeping any sort of inflater cache, reusing it, or setting the factory once to the activity's inflater always caused problems. The solution that worked was to to always clone an inflater, set the factory and inflate.

This is psuedo-code for my wrapper. Robobinding has the Binders class.

public class MyInflater {
   private ViewCreatedListener viewCreatedListener;
   public MyInflater() {}

   public LayoutInflater with(Context context) {
      // cloning the existing inflater. This is important because custom inflaters (like calligraphy)
      // use factory wrappers, so setting it once and checking later is unreliable.
      // we also cannot cache the inflater based on context because that changes.
      LayoutInflater inflater = LayoutInflater.from(this.context).cloneInContext(context);
      inflater.setFactory(new LayoutInflaterFactoryImpl(inflater, this.viewCreatedListener));
      return inflater;
   }
}

Here is the factory I ended up writing:

public class LayoutInflaterFactoryImpl implements LayoutInflater.Factory {
    private static final String[] sClassPrefixList = {
            "android.widget.",
            "android.webkit.",
            "android.app."
    };
    private final LayoutInflater layoutInflater;
    private final ViewCreatedListener listener;

    public LayoutInflaterFactoryImpl(LayoutInflater layoutInflater, ViewCreatedListener listener) {
        this.layoutInflater = layoutInflater;
        this.listener = listener;
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        // allow the inflater to handle this.
        if ("fragment".equals(name))
            return null;

        // handle the class. the layout inflater already passes the class attribute of "view" tags.
        if (name.contains("."))
            try {
                return this.createView(name, attrs);
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
        else {
            // surprisingly, this is how android's own phone layout inflater does it.
            for (String prefix : sClassPrefixList) {
                try {
                    return this.createView(prefix + name, attrs);
                } catch (ClassNotFoundException e) {}
            }

            throw new RuntimeException("Failed to find a class for the given xml tag "+ name);
        }
    }

    private View createView(String name, AttributeSet attrs) throws ClassNotFoundException {
        View view = this.layoutInflater.createView(name, null, attrs);
        this.listener.onViewCreated(view, attrs);
        return view;
    }
}

In my codebase, this setup works very well. I have full compatibility with third-party libraries and my app doesn't choke on layouts with fragment tags.

@weicheng113

This comment has been minimized.

Member

weicheng113 commented Feb 3, 2015

Hi Vangorra,

Thanks a lot. I will have a look.

Best regards,
Cheng
2015年2月3日 11:38 AM于 "vangorra" notifications@github.com写道:

While I do not have a use for robobinding, I stumbled across this thread
while searching for a solution to my own problems. While my solution won't
directly fit into Robobinding's codebase but hopefully this will help
inspire a solution.
At work, I have created a library similar to robobinding. I call
MyInflater.with(content).inflate(...); in order to inflate layouts and bind
to special attributes. There is a bunch of proprietary stuff that happens
within, but you get the idea. The problem is, the view inflation of android
is very delicate and breaks easily. I can empathize with weicheng113.

I tried creating custom combinations of custom ContextWrapper,
LayoutInflater and LayoutInflater.Factory. The results were always some
obscure error about context (stale, old or wrong) or crashes when
attempting to inflate special tags like "fragment". Using custom
ContextWrapper and LayoutInflater has complications in that you run the
risk of breaking compatibility with third-party libraries like Calligraphy.

Ultimately, I was able to get things working by using only a custom
LayoutInflater.Factory. Since I expect my static class to be called for all
inflate actions, I ended up cloning the LayoutInflater and setting the
factory on the new inflater. Keep in mind, my solution was a combination of
the the Factory and how I handled the LayoutInflater/Context. Basically, I
kept the Context/Inflater completely unadulterated, clone the inflater and
set the factory.
Keeping any sort of inflater cache, reusing it, or setting the factory
once to the activity's inflater always caused problems. The solution that
worked was to to always clone an inflater, set the factory and inflate.

This is psuedo-code for my wrapper. Robobinding has the Binders class.

public class MyInflater {
private ViewCreatedListener viewCreatedListener;
public MyInflater() {}

public LayoutInflater with(Context context) {
// cloning the existing inflater. This is important because custom inflaters (like calligraphy)
// use factory wrappers, so setting it once and checking later is unreliable.
// we also cannot cache the inflater based on context because that changes.
LayoutInflater inflater = LayoutInflater.from(this.context).cloneInContext(context);
inflater.setFactory(new LayoutInflaterFactoryImpl(inflater, this.viewCreatedListener));
return inflater;
}
}

Here is the factory I ended up writing:

public class LayoutInflaterFactoryImpl implements LayoutInflater.Factory {
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
private final LayoutInflater layoutInflater;
private final ViewCreatedListener listener;

public LayoutInflaterFactoryImpl(LayoutInflater layoutInflater, ViewCreatedListener listener) {
    this.layoutInflater = layoutInflater;
    this.listener = listener;
}

@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
    // allow the inflater to handle this.
    if ("fragment".equals(name))
        return null;

    // handle the class. the layout inflater already passes the class attribute of "view" tags.
    if (name.contains("."))
        try {
            return this.createView(name, attrs);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    else {
        // surprisingly, this is how android's own phone layout inflater does it.
        for (String prefix : sClassPrefixList) {
            try {
                return this.createView(prefix + name, attrs);
            } catch (ClassNotFoundException e) {}
        }

        throw new RuntimeException("Failed to find a class for the given xml tag "+ name);
    }
}

private View createView(String name, AttributeSet attrs) throws ClassNotFoundException {
    View view = this.layoutInflater.createView(name, null, attrs);
    this.listener.onViewCreated(view, attrs);
    return view;
}

}

In my codebase, this setup works very well. I have full compatibility with
third-party libraries and my app doesn't choke on layouts with fragment
tags.


Reply to this email directly or view it on GitHub
#194 (comment)
.

@vangorra

This comment has been minimized.

vangorra commented Mar 5, 2015

@weicheng113 I did a bit more troubleshooting with my own library and figured out the two key parts that make this work.
The code above will still do the trick but the LayoutInflater you pass to it is key. When you call Context.getSystemService(LAYOUT_INFLATER_SERVICE), the context and all context wrappers have an opportunity to override the OS default behaviour. This is especially important when working with the support library which seems to replace or modify the LayoutInflater that comes from Context.getSystemService(). The key is when requesting the LayoutInflater, you must be using an activity context that supports fragment tags, like FragmentActivity for example. If you use the app context, you will get an exception about not finding android.view.fragment. Using that LayoutInflater and the concepts above should do the trick.

@weicheng113

This comment has been minimized.

Member

weicheng113 commented Mar 5, 2015

@vangorra, Thanks for the information. I will look into it when i am next available.

Thanks, Cheng

@radzio

This comment has been minimized.

radzio commented Apr 21, 2015

Hi,

Any update for this issue?

@weicheng113

This comment has been minimized.

Member

weicheng113 commented Apr 21, 2015

Hi Radek, not yet.
2015-4-21 PM4:36于 "Radek Piekarz" notifications@github.com写道:

Hi,

Any update for this issue?


Reply to this email directly or view it on GitHub
#194 (comment)
.

@vangorra

This comment has been minimized.

vangorra commented Aug 27, 2015

I did a bit more work with Factory classes lately too. Turns out appcompat 22+ breaks functionality for setFactory. Instead, you are expected to use LayoutInflaterCompat.setFactory();

[setFactory()](https://developer.android.com/reference/android/support/v4/view/LayoutInflaterCompat.html#setFactory%28android.view.LayoutInflater, android.support.v4.view.LayoutInflaterFactory%29)

@weicheng113

This comment has been minimized.

Member

weicheng113 commented Aug 27, 2015

@vangorra , The strategy RoboBinding took was to delegate the view creation to original LayoutInflater.factory or factory2, instead of trying to replace or twist. RoboBinding is always working at the moment, even with appcompat 22+. Gallery sample project is an example - https://github.com/RoboBinding/RoboBinding-gallery/blob/master/app/build.gradle. I currently have not found any further issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment