Can't use Class.forName() with v4.app.Fragment - android

I'm trying to run the below code during onResume().
SharedPreferences sharedPreferences = this.getSharedPreferences("com.example.appname.MainActivity", Context.MODE_PRIVATE);
String foregroundFragment = sharedPreferences.getString("foreground_fragment", "");
try {
Fragment fragment = (Fragment) Class.forName(foregroundFragment);
} catch (ClassNotFoundException e) {
Log.i("EXCEPTION!", e.toString());
}
However, Class.forName() appears to be the wrong method since it gives the following error message:
"Inconvertible types; cannot cast 'java.lang.Class>' to 'android.support.v4.app.Fragment.'
I'm not aware of another method that allows me to retrieve a String and use it to initialise a Fragment variable. I've searched online, but haven't been able to find anything other than Class.forName(). The string I'm retrieving via SharedPreferences does contain the fragment's full name, ie. "com.example.appname.ForegroundFragment".
Is there another method I should be using? Am I going about this all wrong?

Try with newInstance()
Fragment fragment = (Fragment) Class.forName(foregroundFragment).newInstance();

Related

Could not find Fragment constructor

I am facing the issue on some devices and getting an error on my crash analytics. A lot of user devices are facing this issue, but on my device it's working fine.
Unable to start activity ComponentInfo{com.ox.outloks.new/com.ox.outloks.new.activities.MainDrawerActivity}: android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment com.alchemative.outfitters.outfitters.fragments.ProductsFragment: could not find Fragment constructor
The error is coming at the line which is in activity super.onCreate(savedInstanceState);
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Here is ProductsFragment constructor
#SuppressLint("ValidFragment")
public ProductsFragment(String id) {
categoryID = id;
}
All Fragment classes you create must have a public, no-arg constructor. In general, the best practice is to simply never define any constructors at all and rely on Java to generate the default constructor for you. But you could also write something like this:
public ProductsFragment() {
// doesn't do anything special
}
If your fragment needs extra information, like String id in your posted example, a common pattern is to define a newInstance() static "factory method" that will use the arguments Bundle to give that info to your fragment.
public static ProductsFragment newInstance(String id) {
Bundle args = new Bundle();
args.putString("id", id);
ProductsFragment f = new ProductsFragment();
f.setArguments(args);
return f;
}
Now, rather than calling new ProductsFragment(id), you'll call ProductsFragment.newInstance(id). And, inside your fragment, you can access the id by calling getArguments().getString("id").
By leveraging the arguments bundle (instead of creating a special constructor), your fragment will be able to be destroyed and recreated by the Android framework (e.g. if the user rotates their phone) and your necessary info (the id) will persist.
Accepted answer is not entirely correct as of today. With FragmentFactory
you can create fragment such as
MyFragment(arg1:Any, arg2:Any,...) {
}
and instantiate it inside FragmentFactory's instantiate method
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return when (className) {
MyFragment::class.java.name -> MyFragment(arg1, arg2)
}
}
and set your FragmentFactory as supportFragmentManager's fragment factory before onCreate of Activity, because Android checks out for empty constructor fragment and if you don't provide before onCreate your app will crash as usual behavior.
supporFragmentManager.fragmentFactory = yourFragmentFactoryInstance
A newer alternative to communicate between an Activity and a Fragment, thus allowing that the Fragment can have an empty constructor would be the use of ViewModels.
I have just faced this exception. My mistake was calling requireArguments() method before arguments were received by the Fragment
The problem comes from below method of fragment class that basically shows how we should basically do initialisation of our fragment classes in general. First see that method below:-
public static Fragment instantiate(#NonNull Context context, #NonNull String fname,
#Nullable Bundle args) {
try {
Class<? extends Fragment> clazz = FragmentFactory.loadFragmentClass(
context.getClassLoader(), fname);
Fragment f = clazz.getConstructor().newInstance();
if (args != null) {
args.setClassLoader(f.getClass().getClassLoader());
f.setArguments(args);
}
return f;
} catch (java.lang.InstantiationException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (IllegalAccessException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (NoSuchMethodException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": could not find Fragment constructor", e);
} catch (InvocationTargetException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": calling Fragment constructor caused an exception", e);
}
}
So if you will see problem lies with catch (NoSuchMethodException e) code block which triggers in this case since its not able detect contructor of the fragment from line Fragment f = clazz.getConstructor().newInstance();. If you will see the function getConstructor(), You will observe this will reponsible to make this exception since it throws this NoSuchMethodException and same have caught inside Fragment instantiate funtion. Also if you move further on above function the recommended approach for sending params to a fragment arguments is also given. So now we are all clear about what to do.
To send data into fragment we should make instance/static function of that receiver fragment, and put all required params into this.
Then put data using fragment arguments into receiver fragment within instance function.
Finally just get those arguments into onCreate/onCreateView.
Note: This Fragment class was deprecated in API level 28.
Use the Jetpack Fragment Library Fragment for consistent behavior across all devices and access to Lifecycle.
It can also happen if you call requireContext() method before onCreate() of the fragment.
I just putted this overrided onCreate inside Myfragment, that extends Fragment. And it works. But i think, it's kind of a plug.
#Override
public void onCreate(Bundle savedInstanceState) {
MyFragment.newInstance();
Bundle b = new Bundle();
super.onCreate(b);
}
Make sure the fragment class is public as well

create an instance of Activity with Activity class object

I have got an Activity class by:
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
String activityClassName = launchIntent.getComponent().getClassName();
Class<?> activityClazz = Class.forName(activityClassName);
Is it possible to create an instance of this Activity by using the activityClazz ? If so, how?
(My code is in a independent java class. Not in activity or service. )
Technically, you can create an instance of an Activity like this. However, this instance would be useless because its underlying Context would not have been set up.
The rule is that you should never ever create instances of Android components (Activity, Service, BroadcastReceiver, Provider) yourself (using the new keyword or other means). These classes should only ever be created by the Android framework, because Android sets up the underlying Context for these objects and also manages the lifecycle.
In short, your architecture is flawed if you need to create an instance of an Activity like this.
Class.forName() needs the fully qualified name - that is, the name of the package the class is contained in, plus the simple name of the class itself.
Assuming the package containing the class is called com.your.package, the code would have to be
String className = "com.your.package.Tab3"; // Change here
Object obj= null;
try {
obj= Class.forName(className).newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
yes. you can get the activity context by using below line of code
Activity activity = (Activity) getContext();

Android : SharedPreferences and MVC pattern

I'm developping my app using the MVC pattern. To store/access data, my controllers use a class named "DataStorage", and for now this class allows to store/access simple parameters about my app (username, data storage path, ...). In other words, I want to call a few methods like "getParameter(String key)" or "setParameter(String key, String value)".
I think SharedPreferences would be the most convenient way to store these parameters, so my get/setParameters use this class.
In all the examples I have seen, SharedPreferences is called from an Activity and there is no problem to call methods such as "getSharedPreferences" or "getApplicationContext"/"getContext". Because my DataStorage class is not an activity, for now I ask my first activity to give its context when creating a new DataStorage instance, and it works well to store my parameters. My problem : I want to be able to remove parameters from another activity using clear + commit methods. But it doesn't work (parameters are still there), and I think the reason is I give the 2nd activity context when creating another instance of DataStorage. The problem might be something else though, I've been practicing Android for only 2 days now...
To summarize how my app works :
Activity 1 creates a DataStorage class and provides its context to the DataStorage constructor. The DataStorage might store a parameter into a SharedPreferences file (or not...)
When I run my app again, if a particular parameter is set in the SharedPreferences file, then I start Activity 2 instead of Activity 1. Using the menu on Activity 2, I want to be able to clear the SharedPreferences file (in order to get Activity 1 again when I restart the app), so I create another DataStorage instance (and I provide Activity 2 context) and I call the method to clear all parameters.
As I said, first part works well (I can store parameters), but clear & commit do nothing to my SharedPreferences file.
I don't want to put a piece of code for this directly in my activities.
Can you help me with this ? What am I doing wrong in the way I use SharedPreferences ?
Thank you for your help !
Edit :
public class DataStorage {
private Context context;
private String settingsFilename;
private SharedPreferences settings;
public DataStorage(Context activityContext, String filename) {
context = activityContext;
settingsFilename = filename;
settings = context.getSharedPreferences(settingsFilename, Context.MODE_PRIVATE);
}
public void newSharedPreference(String key, String value) {
settings.edit().putString(key, value).apply();
settings.edit().commit();
}
public String getSharedPreference(String key) {
return settings.getString(key, null);
}
public void clearPreferences() {
settings.edit().clear();
settings.edit().commit();
Toast.makeText(context,settings.toString(), Toast.LENGTH_LONG).show();
}
}
In my first activity (the code is part of onCreate method) :
DataStorage storage = new DataStorage(this, getResources().getString(R.string.sharedPreferencesFile));
username = storage.getSharedPreference("username");
Toast.makeText(this, username, Toast.LENGTH_LONG).show();
if (username != null) {
Intent nextActivity = new Intent(this, ActivityMainMenu.class);
startActivity(nextActivity);
} else {
setContentView(R.layout.activity_name);
}
In my 2nd activity :
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
switch(id) {
case R.id.action_clearSharedPref :
storage.clearPreferences();
break;
case R.id.action_leave :
System.exit(RESULT_OK);
}
return super.onOptionsItemSelected(item);
}
(Storage is constructed exactly as I did in my first Activity)
I tried to replace "this" by getApplicationContext() in DataStorage constructor, but it didn't work.
From Editor Class Overview
Interface used for modifying values in a SharedPreferences object. All changes you make in an editor are batched, and not copied back to the original SharedPreferences until you call commit() or apply().
You need to update to change methods at your DataStorage
public void newSharedPreference(String key, String value) {
settings.edit().putString(key, value).apply();
}
and
public void clearPreferences() {
settings.edit().clear().apply();
Toast.makeText(context,settings.toString(), Toast.LENGTH_LONG).show();
}
Reason of issue is next
settings.edit().clear(); // clear is ok, but it won't be saved because
settings.edit().commit(); // create new editor and commit nothing

Cannot save Shared preferences, it crashes because the context is null, fragment already replaced

The app was first made with activities, but because of an issue, I have to use 2 fragments with an FragmentManager instead. Now, because of this I had to refactor a lot of code.
So my 2 fragments are an videoplayerfragment and videoRecorderFragment.
Sometimes I send an request to get via a JSON, the list of videos in the videoPlayer, and if I want to make a video, I change the fragment. Now if the response is late, I will get the OnSuccess of the request, trying to save the credentials, but the context of the videoplayer is null, because I have replaced the fragment.
This is part of the UserCredentialsPersistence class:
private static SharedPreferences obtainSharedPreferences(Context context) {
return context.getSharedPreferences(USER_CREDENTIALS_KEY,
Context.MODE_PRIVATE);
}
public static boolean saveToDownloadCount(Context context,
int download_count) {
Editor e = context.getSharedPreferences(USER_CREDENTIALS_KEY,
Context.MODE_PRIVATE).edit();
e.putInt(USER_TO_DL_COUNT_KEY, download_count);
return e.commit();
}
This is the important part of the VideoPlayerFragment:
videoRequest.requestNotification = new RequestNotification() {
#Override
public void onSuccess(Object sender) {
Log.d("#VideoPlayerActivity", "success");
playHideUpAnimation();
new_videos = new ArrayList<VideoData>(VideoDataManager.getInstance().getNewVideos());
UserCredentialsPersistence.saveToDownloadCount(getActivity(), UserCredentialsPersistence.restoreToDownloadCount(getActivity()) + new_videos.size());
all_videos = videodb.getTotalVideoListFromDB();
Here it crashes because the getActivity is null when calling any method from UserCredentialsPersistence.
I have also tried to call it from the FragmentManager:
OnSuccess at the requestNotification calling:
((VideoHolderActivity) getActivity()).saveToDLCount(new_videos.size());
And this being:
public void saveToDLCount(Integer size){
UserCredentialsPersistence.saveToDownloadCount(VideoHolderActivity.this, UserCredentialsPersistence.restoreToDownloadCount(VideoHolderActivity.this) + size);
}
But still, the context is null. Any idea how I can get past this?
I created a BaseFragment class. All my Fragments extend this class. And in this class I have a
interface, which is implemented by my FragmentNavigator.
Then i simply call from the fragments the functions that i need from the FragmentManager. Here i have put all the functions that need context. And after getting the response from them, i send it back into the fragments, by doing a cast to the current fragment i use.

ClassCastException while dynamically loading a class in Android

I have a thread that loads different classes for the resources it needs depending on the specific implementation of the system. My implementation is on Android and I have a class that returns the specific classes needed by my implementation. I seem to be able to load the class fine, but when I try to assign it to the object in my main thread, it gives me a ClassCastException. Here are the snippets:
In my main thread, I do:
try {
grammarProcessor = config.loadObject(GrammarProcessor.class);
which gives me this stacktrace:
E/AndroidRuntime(6682): FATAL EXCEPTION: JVoiceXmlMain
E/AndroidRuntime(6682): java.lang.ClassCastException: org.jvoicexml.android.JVoiceXmlGrammarProcessor
E/AndroidRuntime(6682): at org.jvoicexml.JVoiceXmlMain.run(JVoiceXmlMain.java:321)
GrammarProcessor is an interface and JVoiceXmlGrammarProcessor is the class that I load and implements that interface. The loading code is as follows:
else if(baseClass == GrammarProcessor.class){
String packageName = "org.jvoicexml.android";
String className = "org.jvoicexml.android.JVoiceXmlGrammarProcessor";
String apkName = null;
Class<?> handler = null;
T b = null;
try {
PackageManager manager = callManagerContext.getPackageManager();
ApplicationInfo info= manager.getApplicationInfo(packageName, 0);
apkName= info.sourceDir;
} catch (NameNotFoundException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
return null;
}
PathClassLoader myClassLoader =
new dalvik.system.PathClassLoader(
apkName,
ClassLoader.getSystemClassLoader());
try {
handler = Class.forName(className, true, myClassLoader);
return (T) handler.newInstance();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
}
When debugging, I check what's returning from the load method and it is an object with an id number. If I click on it, it'll say org.jvoicexml.android.JVoiceXmlGrammarProcessor#40565820, and the dropdown will show the two private fields that a JVoiceXmlGrammarProcessor should have, so it looks like it's well loaded. Any ideas?
I think I understand what's happening here but I have to make an assumption that org.jvoicexml.android is not your package, i.e., you're loading from a different apk (as the bounty seems to suggest).
With that in mind, this is impossible and for a good reason.
Let's start with your own app - you have the type GrammarProcessor available from your own classes.dex and into your default ClassLoader (the PathClassLoader that you get when the zygote forks your process). Let's call this type GP1. Any class in your own application that implements GrammarProcessor actually has GP1 in their interface list.
Then, you instantiate a new classloader. If you look at the source, you'll see that PathClassLoader is just a thin wrapper around BaseDexClassLoader which in turn delegates to a DexPathList, which in turn delegates to DexFile objects which in turn do the loading in native code. Phew.
There's a subtle part of BaseDexClassLoader that's the cause of your troubles but if you haven't seen it before, you might miss it:
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
and a bit further down:
#Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class c = pathList.findClass(name);
if (c == null) {
...
}
return c;
}
BaseDexClassLoader does not check with its parent first!
.. and that in short is your problem.
More precisely, the DexPathList and DexFile inside it load all the classes from the other dex and never look into the classes already loaded in the VM.
So, you end up with two different loaded versions of GrammarProcessor. Then, the object you're instantiating is referring to the new GP2 class, while you're trying to cast it to GP1. Obviously impossible.
Is there a solution to this?
There's one that's been done before, but you won't like it. Facebook use it in their app to load a bunch of dex files with strong relationships between them. (It's there, before all the messing about with LinearAlloc):
we examined the Android source code and used Java reflection to directly modify some of its internal structures
I'm 90% sure they get the PathClassLoader that you're given (getSystemClassLoader()), get the DexPathList and override the dexElements private field to have an extra Element with the other dex file (apk in your case). Hacky as hell and I would advise against it.
It just occurred to me that if you don't want to use the newly loaded classes in a way that the framework sees them, you could extend from BaseDexClassLoader and implement the proper look-in-parent-before-trying-to-load behaviour. I haven't done it, so I can't promise it will work.
My advice? Just use remote services. This is what Binder is meant for. Alternatively, rethink your apk separation.

Categories

Resources