Ok - preface with I am new to android and new to java as well. But I did code in a previous lifetime.....
I am working on an application and now trying to pull some methods out and place into a utility class. In particular, I have a method which updates text views that I wanted to move out of an activity.
When in the activity, I had two versions of the method the only difference being that one would accept a view in the parameter list (I used this to populate some fields in a custom dialog). They all worked fine.
Once placed in the external utility package/class, the method no longer works - no errors, and it appears to have all it needs - I've done some logging and the view claims to be visible and the textview ids appear to be correct. Yet nothing changes on the screen.
I'm guessing this is something completely obvious and stupid but I can't seem to sort it out.
package xxx.xxx.Utility;
(some imports)
public class Utility {
public static void updateTextView(int id, String opt_data, View v) {
String TAG = "updateTextView: ";
if (v.getVisibility() == View.VISIBLE) Log.i(TAG," visible");
TextView tvTarget = (TextView) v.findViewById(id);
if (tvTarget == null) {
Log.i(TAG, "Error: updateTextView target is null");
}
if (opt_data != null) {
if (tvTarget != null) {
tvTarget.setText(opt_data);
}
} else {
if (tvTarget != null) {
tvTarget.setText(" ");
}
}
}
}
EDIT w/ Additional Info:
In the inital description I mentioned that this method was also being used to populate some fields of a pop-up dialog with data. In fact, I can request any number of dialogs in that manner and they all display properly and with the correct (and different) data. So it seems to fail only when trying to update the tv data of the main activity (the initial) view.
I'm guessing this is something completely obvious and stupid but I
can't seem to sort it out.
It helps to get the root(?) parent (?) view properly. IE,
currentView = this.findViewById(android.R.id.content).getRootView();
and now all is well.
Related
In my application I have one Fragment which is responsible for displaying a list of news items. It takes a String parameter which determines which url to pull data from.
I set the Fragment with this code:
private void setFragment(String pageToLoad, NewsFeedFragment newsFeedFragment) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
if(newsFeedFragment == null) {
transaction.replace(R.id.container, NewsFeedFragment.newInstance(pageToLoad), pageToLoad);
}
else {
transaction.replace(R.id.container, newsFeedFragment, pageToLoad);
}
mPageToLoad = pageToLoad;
}
In my parent Activity I keep track of which 'page' is currently being viewed:
protected void onSaveInstanceState(#NonNull Bundle outState) {
if(mPageToLoad != null) {
outState.putString("pageToLoad", mPageToLoad);
}
super.onSaveInstanceState(outState);
}
In my parent Activity onCreate method I check whether an instance of NewsFeedFragment has been created and added to the FragmentManager as follows:
protected void onCreate(Bundle savedInstanceState) {
if (savedInstanceState != null) {
if (savedInstanceState.containsKey("pageToLoad")) {
String pageToLoad = savedInstanceState.getString("pageToLoad");
if(pageToLoad != null) {
NewsFeedFragment newsFeedFragment = (NewsFeedFragment) getSupportFragmentManager().findFragmentByTag(pageToLoad);
if(newsFeedFragment != null) {
setFragment(pageToLoad, newsFeedFragment);
}
else {
setFragment(pageToLoad, null);
}
}
}
}
}
This works well 99% of the time, the application resumes correctly and displays the last instance of NewsFeedFragment added. However, I have an issue which seems to occur randomly where the RecyclerView Adapter in NewsFeedFragment is sometimes null when the Fragment is retrieved from the FragmentManager using the findFragmentByTag(pageToLoad) method.
In NewsFeedFragment the RecyclerView Adapter is a class variable:
public NewsPageAdapter mNewsPageAdapter;
The onActivityCreated method of NewsFeedFragment is as follows:
public void onActivityCreated(Bundle savedInstanceState) {
if(mNewsPageAdapter == null) {
Log.i(TAG, "mNewsPageAdapter is null"); // This is logged when issue occurs
}
if(savedInstanceState == null || mNewsPageAdapter == null) {
new LoadFirstPageTask().execute(); // Fetches news items from web service, creates mNewsPageAdapter, and then calls setupRecyclerView() method
}
else {
setupRecyclerView(savedInstanceState);
}
}
Finally, this is the NewsFeedFragment setupRecyclerView method:
private void setupRecyclerView(Bundle savedInstanceState) {
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setAdapter(mNewsPageAdapter);
}
From what I've described could anyone offer any insight as to why the NewsPageAdapter may sometimes be null when the Fragment is retrieved from The FragmentManager?
Thanks
Ok, so you do new LoadFirstPageTask().execute(); and I guess that will eventually call setupRecyclerView?
I think you're over complicating a solution here.
You have all these conditions, business logic, and decisions made inside a volatile component (a Fragment) whose lifecycle is quite complex and not always what you'd expect; you also couple the maintenance of asynchronous data to this structure, and this is having unexpected side-effects that are hard to pin point and track down.
Creating the Adapter is "cheap" compared to fetching, processing, and producing the data for said adapter.
You don't seem to mention ViewModels anywhere, are you using a viewModel? Or any other sort of pattern like a Presenter, Interactors, useCases?
AsyncTasks are also Deprecated and while I don't advocate to run to the "refactor" hill every time a class is deprecated, I think you could get a better and more stable, testable, and readable solution if you abstract that AsyncTask into a coroutine (all managed by your ViewModel for example).
To put in other terms, your Fragment and Activity shouldn't have to deal with the logic regarding "do I need to load this data or not"; this is someone else's responsibility.
About your code.
Ok, now that I've ranted about how you're doing things, let's dig deeper into your existing Java code.
the RecyclerView Adapter in NewsFeedFragment is sometimes null when the Fragment is retrieved from the FragmentManager using the findFragmentByTag(pageToLoad) method.
Whenever we see "sometimes" in a crash, the 1st suspect should be timing/threading. Synchronous code can fail, but it's often orders of magnitude more predictable than Asynchronous code.
If it's "sometimes" null, then the task that is in charge of changing this behavior is not always ready by the time it's needed; or the condition needed for this task to run, is not always what you expect by the time it's checked, and so on, and so forth.
Start by re-architecting your idea into a separate component.
Have the Fragment create its adapter (can be empty of data) as soon as possible, regardless of whether there's data or not.
Have the fragment ask another component for the data. And when the data is available, send it to the Fragment who will in turn set it in the adapter. If the data is already there by the time you ask for it (because you "cached" it), you won't have to wait.
I'd also store the "last viewed page" in the same component, so you don't need to save the state and pass it alongside to a fragment. Rather the fragment asks for "the current data" and the component already knows what it is.
All in all, it's a bit difficult to put all your pieces together because we, the readers, don't have all the code, nor your requirements that lead you to this solution.
I have a project using Realm.io for storing entities, this used to work fine, but now I have a fragment or activity containing 3 fragments with Lists of Realm objects.
Whenever I Switched to a page and back to the first one (or whatever just returning to a page). I get the java.lang.IllegalStateException: Illegal State: Row/Object is no longer valid to operate on. Was it deleted?
This seems to occur because the objects are no longer valid. Is there a simple way to detach them or something? Allthough it would be nice if they remain managed since I sometimes want to be able to delete them.
The items are queried from database, when there are not sufficient items they will get loaded from the API. Nothing extremely fancy is being used here, three lists with adapters which load the entities. THe difference per list is a string value status, which says if it's an certain status.
I get the error when I load the item from the Adapter after clicking the list item to show the details:
MyEntity myEntity = (MyEntity) adapter.getItem(position);
intent.putExtra("id", myEntity.getId()) <-- this part will crash it.
with exception:
java.lang.IllegalStateException: Illegal State: Row/Object is no longer valid to operate on. Was it deleted?
I guess it's because it's querying the same type of data on three locations (3 tabs). Though I would expect this not to be a problem since they all have their own adapter and list of items. Fetched from their own instances.
This is the code being called by my "Loader" class which handles the from DB and/or Api fetching.
public void loadResult(List result, boolean isFinished) {
//not the best for speed, but at a max of 10 items this is fine to not get duplicates and respect the original order
try {
for (RealmObject ro : result) {
Record r = (Record) ro;
int itemIndex = items.indexOf(r);
if (itemIndex > -1) {
items.set(itemIndex, r);
} else {
items.add(r);
}
}
} catch (IllegalStateException e) {
ErrorClass.log(e);
}
notifyDataSetChanged();
setLoading(!isFinished);
end = result.size() < 10 && isFinished;
}
in short the loader class does this, and it's not a singleton, it's a new instance per Listview (Recycler)
List result = null;
if (sortKey != null) {
result = query.findAllSorted(sortKey, ascending);
} else {
result = query.findAll();
}
if (result.size() < PAGE_SIZE && retry == 0) {
isFinished = false;
retry++;
getPageFromApi(pageNumber);
} else if (retry > 0) {
retry = 0;
}
adapter.loadResult(result, isFinished);
The getPageFromApi will result on this code being called again, and existing entities will be replaced in the list, new items added. So no old removed items should exist in the list when clicking them.
I think this might be very specific but there must be a global reason/solution to my problem.
Stupid me, I wrapped the adding of the new elements in a try catch because of the error before, what was going wrong is pretty simple. In the Loader the items fetched from our API was updating or creating new items. Meaning that those in the list, will be invalid at that point, or at least the pointers to them? Not sure how it works behind the scenes.
What I did to fix it, was loop through all the current items, and check the isValid(), if false the item would be removed. Otherwise I was checking for a new item to be inside the current items List, which would cause the error to occur in the .equals function!
This one thing is something that might be a core error, but I think it's just my error!
This is probably very odd, but I'm using multiple CursorLoaders in Android to do multiple queries and in the onLoadFinished(), I am adding views like TextViews and ListViews to my layout dynamically based on cursor results like if the cursors were not null. I do get accurate results, but since I'm using AsyncTaskLoader (CursorLoader), the cursor results don't come in at the same time and the results are not added in the correct order. I previously used a static layout and added views at indices and did view.setVisiblity(View.GONE) based on the results, but it was just too much and too confusing because I have like 32 views. Plus it seemed weird because I don't think the user wants to see all of those views going away and moving up and down based on AsyncTaskLoader results.
How can I get the views in the correct order I want them in without having a bunch of boolean variables? I looked into LayoutInflater but that requires indices as well, but I'm not sure that will help me. The problem with indices for me is that in cursorloader ID 1:
view.addView(v, 1);
view.addView(v, 2);
might not get executed until the cursorloader with ID 2 finishes with:
view.addView(v, 3);
view.addView(v, 4);
If cursorLoader ID 1 doesn't get executed and ID 2 does, then there is missing space and I have to do a ton of view.setVisibility(View.GONE) if I use static XML views and do not dynamically add them.
In the code I'm doing something like this currently:
#Override
public void onLoadFinished(android.support.v4.content.Loader<Cursor> cursorLoader, Cursor cursor) {
switch (cursorLoader.getId())
{
case 0:
if (cursor != null && cursor.moveToFirst()) {
..
title = new TextView(this);
...
mainLinearLayout.addView(title, 1);
}
break;
case 1:
if (cursor != null && cursor.moveToFirst()) {
..
title2 = new TextView(this);
mainLinearLayout.addView(title2, 2);
break;
default:
...
}
}
I also read somewhere online that it is better to use a service instead of cursorloader if you want to do queries on the background thread and have them finish in a certain order, but I have not heard that advice anywhere else or seen any examples doing queries in services. They all use CursorLoader. Is this advice necessarily true? Sounds a bit sketchy.
By the way, I am using the CursorLoader implementation without a content provider given at CursorLoader usage without ContentProvider
How can I get the views in the correct order I want them in without
having a bunch of boolean variables?
You do need some sort of status control in order to make the views appear in order. I would delegate the view construction/addition to a control class that will have all the information required to make the correct view and in the right order no matter how the loaders finished their jobs.
public class ViewDispatcher {
public SparseArray<Status> mLoadStatuses = new SparseArray<Status>();
public SparseArray<Cursor> mDataCursors = new SparseArray<Cursor>();
// you'll call this method for each of the loaders, in the order they should be. The ids should be incremental
public void registerLoader(int loaderId) {
mLoadStatuses.put(loaderId, Status.INITIAL);
}
// called when one of the loaders finishes its job
public void onLoadComplete(int loaderId, Cursor data) {
mDataCursors.put(loaderId, data);
boolean current = true;
mLoadStatuses.put(loaderId, Status.LOADED);
if (loaderId == firstLoaderId) {
// the first loader it's done and we should start the view creation right away
buildView(loaderId, mainLayout, true);
mLoadStatuses.put(loaderId, data, Status.FULLY_BUILT);
} else {
// implement a priority system, a view construction will be triggered ONLY
// if the previous loader has finished loading data and its view is in place
// I'm assuming that the Loaders have consecutive ids
if (mLoadStatuses.get(loaderId - 1) != null && mLoadStatuses.get(loaderId - 1) == Status.FULLY_BUILT) {
buildView(loaderId, data, mainLayout, true);
mLoadStatuses.put(loaderId, Status.FULLY_BUILT);
} else {
current = false;
}
}
// we'll also need to implement a buddy system. When a loader is done loading and its view
// is created we must check to see if we don't have other loaders after this current one that have finished loading
// but couldn't construct their view because this current loader didn't finished at that moment
// get the next loader
int next = loaderId + 1;
while(current && next < totalNumberOfLoaders && mLoadStatuses.get(next) == Status.LOADED) {
// continue to build views
buildView(next, mDataCursors.get(loaderId), mainLayout, true);
mLoadStatuses.put(next, Status.FULLY_BUILT);
next++;
}
}
// this will build the appropriate view, and optionally attach it
public void buildView(int loaderId, Cursor data, view mainLayout, boolean attach) {
// build the view for this specific Loader
}
}
public enum Status {
INITIAL, LOADED, FULLY_BUILT
}
I hope I'm not missing something obvious as I wrote that without any tests. To use it, you'll first call the registerLoader() method for all loaders in the order you need them to be and in the onLoadComplete() callback of the LoaderCallbacks call ViewDispatcher.onLoadComplete().
I also read somewhere online that it is better to use a service
instead of cursorloader if you want to do queries on the background
thread and have them finish in a certain order, but I have not heard
that advice anywhere else or seen any examples doing queries in
services.
You've probably read about IntentService which can be made to follow a queue through the order of the Intents it receives. But, I don't see how this would help you as it would just add problems. For one you use Cursors as the data holders that you would need to pass back and you need to create views which the IntentService can't do(it will need to make the Activity create them through various communication ways, this is unnecessary work from my point of view).
I've tried the following approaches to try to get the TextView to update from the model:
TextChangeListeners - error
Other Threads
asynctask
Added a refresh button which updates the data, I force the click
Scenario is as follows:
Standard thread running which updates the Model (MVC) object every second, which works perfectly.
When the Model object is updated, it notifies all classes which implement "Observer".
My GolfHomeScreen extends Activity and implements Observer (code below).
The GolfHomeScreen.update (Observable observable, Object data) method works perfectly. It executes every second as expected and S.O.P the correct data (see --> //* #1 *).
This then kicks off a Thread. I read on this site that you have to UI Thread to execute code which updates widgets and then run the "runOnUiThread" method - the code looked something like I have done below but I found it a little hard to follow. Anyway, this thread runs (see --> //* #2 ) which then executes method onClick(View v) (see --> // #3 *).
System.out.println("driverType :" + mvcModel.getDriverName()); (see --> //* #4 *) works perfectly.
//* #5 * - it MUST update but it doesn't.
If I actually physically click the button, the data is displayed correctly on the screen. My only guess is that the screen must not refresh on the forced click or else I am not using the UI thread to make the update.
NOTE: If the code below is missing something, it is because I removed a ton of code to simplify the understanding of what's going on.
public class GolfHomeScreen extends Activity implements Observer
{
GolfHomeScreen golfHomeScreen = null;
TextView driverName = null; // Type of driver used by golfer (eg: 1 wood)
Button refreshData = null;
#Override public void onClick(View v) //*** #3 ***
{
if ((v.getId()) == 12345) // ID of wood (12345 = 1 wood)
{
//*** #4 ***
System.out.println("driverType :" + mvcModel.getDriverName()); //THIS PRINTS OUT THE LATEST DATA!!!
//*** #5 ***
driverName.setText(String.valueOf(mvcModel.getDriverName())); //THIS DOESN'T AFFECT THE SCREEN?? WHY :-(
}
}
#Override public void onCreate(Bundle savedInstanceState)
{
golfHomeScreen = this;
refreshData = indViewById(R.id.golfhomescreen_button_refreshData);
driverName = (TextView) findViewById(R.id.golfhomescreen_text_drivername);
}
#Override public void update(Observable observable, Object data)
{
// #1 ***
System.out.println("driverType :" + mvcModel.getDriverName());
golfHomeScreen.runOnUiThread(new Runnable()
{
#Override
public void run()
{
refreshData.performClick(); //*** #2 ***
}
});
}
}
I'll run a few suggestions and a few fixes and hopefully your problem will be gone.
I'm saying that because conceptually you do have some wrong stuff, and my answer is kind of fixing those concepts:
remove GolfHomeScreen golfHomeScreen = null; and golfHomeScreen = this; you don't need an object referencing to itself. It's just making confusion. This line golfHomeScreen.runOnUiThread(new Runnable() you should just call runOnUiThread(new Runnable()
Change System.out.pr*** to Log.d(TAG, MESSAGE); that's just because that is Android specialised way of dumping logs and it won't really matter for your result. It's nicer than standard Java System.out because allows you to filter those logs by TAG and severity.
Do never call refreshData.performClick(); or anything similar to that because you want the action inside that button to be performed. If you want a certain thing to happen from more than one source, create a method doThatThing(), put the actions in there, and from the Click and from the Update, you call the method. The way you're doing is a bit of a "Rube Goldberg machine" that is updating something, to click a button, to change a text. Just change the text.
do never compare a view ID with a number if ((v.getId()) == 12345) the ID of views created from XML are generated by the system during compilation and you can't know if it's a fixed number. You should compare with the static int ID of that view, like this : if(v.getId() == R.id.golfhomescreen_text_drivername). If the view was not created in XML (which is not the case here), you can compare with the actual object if(v.equals(driverName))
with those changes I believe your code will work. Make sure to let me know and mark as correct answer if in fact does.
So I'm still working on my first little app here, new to Android and Java, so I'm stuck on a basic little problem here. Answers to my first questions were really helpful, so after researching and not coming up with anything, I thought I'd ask for some more help!
The idea is that on another screen the user makes a choice A, B, C, or D, and that choices is passed as a string through the intent. OnResume checks if the choice is not null and sets an integer that corresponds to that string. Later when the user pushes another button, some if else logic checks that int and performs and action based on which was chosen. The problem is that the App crashed at onResume.
I learned that I have to use equals(string) to compare string reference, but maybe the problem is that I am trying to compare a string in reference to a literal string? Any help would be greatly appreciated.
Thanks!
protected void onResume() {
super.onResume();
// Get the message from the intent
Intent intent = getIntent();
String choice = intent
.getStringExtra(ExtensionSetupSlector.TORQUE_SETUP);
// Create the text view
TextView displayChoice = (TextView) findViewById(R.id.displayChoice);
if (!choice.equals("")){
displayChoice.setText(choice);
if (choice.equals("A")) {
myChoice = 1;
}
if (choice.equals("B")) {
myChoice = 2;
}
if (choice.equals("C")) {
myChoice = 3;
}
if (choice.equals("D")) {
myChoice = 4;
}
}
}
myChoice is declare right after ...extends Activity{ Also I'm not quite sure If this should really be in onResume, but it was working before I started try to set myChoice in the onResume (when I was just displaying the choice). Thanks again!
Change if (!choice.equals("")) to check for null instead. Otherwise your app attempts to access an empty reference and crashes.