RecyclerView Adapter not updating views after device rotation - android

I have a chat app, which opens a Websocket to a server. The app has 2 modules -
chat sdk: which handles the connection plus back and forth
messaging.
ui: which handles adapters and views
The chat UI is on a fragment hosted by an activity.
Everything worked fine but when i decided to use persistence on device rotation, i have problems.
When i rotate the device, the websocket reconnects (which is normal),the activity is recreated so is the fragment too.
After device rotation and some debugging, i saw that my messageList is properly filled with any added items (ie message texts) but not the UI. When i type a text from the EditText and press send, the item is properly displayed in the UI, BUT when i receive response from server the UI is not updated even if the message is added to the messageList.
I read in similar posts that the RecyclerView adapter is updating the old fragment after the rotation. Is this the case? And if this is the case, why when i send a message from the EditText it is properly displayed but not when from the onMessage of my WebSocket?
I tried "android:configChanges="screenSize|orientation|screenLayout" w/o any results.
I override the onConfigurationChanged and force the adapter to update the views, with no result
#Override
public void onConfigurationChanged(#NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if(messagesAdapter!=null){
messagesAdapter.notifyDataSetChanged();
}
}
I save the fragment to restore it in my activity
public class MessageActivity extends AppCompatActivity {
private MessageFragment fragment;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_message);
if (savedInstanceState == null) {
fragment = MessageFragment.newInstance();
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, fragment)
.commitNow();
}else{
fragment = (MessageFragment) getSupportFragmentManager().getFragment(savedInstanceState, "messageFragment");
}
}
#Override
public void onBackPressed() {
finishAffinity();
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
//Save the fragment's instance
getSupportFragmentManager().putFragment(outState, "messageFragment", fragment);
}}
And i also save and retrieve my messageList
#Override
public void onSaveInstanceState(#NonNull Bundle outState) {
super.onSaveInstanceState(outState);
//saving the state of the chat
outState.putParcelableArrayList("messages", messageArrayList);
try {
Log.d(TAG, "messagelist in save instance " + messageArrayList.size());
}catch (NullPointerException e){
e.printStackTrace();
}
outState.putString("dialogId", messageArrayList.get(0).getDialogId());
}
onMessage from websocket
#Override
public void onMessage(ArrayList<Message> data) {
if (!messageArrayList.isEmpty() && messageArrayList.get(messageArrayList.size() - 1).getServerMessageType() == Constants.TYPE_TYPING_ON) {
messageArrayList.remove(messageArrayList.size() - 1);
}
this.messageArrayList.addAll(data);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && isFragmentAttached) {
mActivity.runOnUiThread(new Runnable() {
#Override
public void run() {
if (omiliaClient != null) {
updateUi(messageArrayList, omiliaClient.user2);
}
}
});
}
}

Call setRetainInstance(true) in either of onCreate(), onCreateView() or onActivityCreated() methods.

Related

Restore Recycler View state on device's rotation

I have RecyclerView which views are populated from the internet (page number is a parameter) and that is why I used the EndlessRecyclerViewScrollListener. The only problem is that when I rotate the device, it doesn't restore the same state.
This is my ScrollListener initiation in onCreate():
mScrollListener = new EndlessRecyclerViewScrollListener(mGridLayoutManager) {
#Override
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
mPage++;
new TheMovieRequestTask().execute();
}
};
mRecyclerView.addOnScrollListener(mScrollListener);
onSaveInstanceState():
protected void onSaveInstanceState(Bundle outState) {
outState.putInt("mPageKey", mPage);
mListState = mRecyclerView.getLayoutManager().onSaveInstanceState();
outState.putParcelable("mListStateKey", mListState);
super.onSaveInstanceState(outState);
}
At the beginning of onCreate() I have:
if (savedInstanceState != null) {
mPage = savedInstanceState.getInt("mPageKey");
mBundleRecyclerViewState = new Bundle();
mListState = savedInstanceState.getParcelable("mListStateKey");
mBundleRecyclerViewState.putParcelable("mListStateBundleKey", mListState);
} else {
mPage = 1;
}
And when I load data from TheMovieRequestTask() (it's an AsyncTask), in onPostExecute() i have:
if (mBundleRecyclerViewState != null) {
Parcelable state = mBundleRecyclerViewState.getParcelable("mListStateBundleKey");
mRecyclerView.getLayoutManager().onRestoreInstanceState(state);
}
What Can I do? When I go back from another activity, it's perfect but on rotation, it just doesn't work.
This code loads e.g. 5th page and then properly restore it but then I can scroll only down (6,7,8,9... pages) but not up(not 4,3,2,1, pages).

Store and restore RecyclerView position not working

I am using GridLayoutManager as LayoutManager for RecyclerView inside a fragment , and RecyclerView adapter get populated by loader.
I tried to store and restore state of GridLayoutManager on device rotation but not working and still get back to the initial state "first element in RecyclerView".
onSaveInstanceState method:
#Override
public void onSaveInstanceState(Bundle outState) {
mRecylcerViewParecelable = mGridView.getLayoutManager().onSaveInstanceState();
outState.putParcelable(GRID_LAYOUT_PARCEL_KEY, mRecylcerViewParecelable);
super.onSaveInstanceState(outState);
}
onViewStateRestored method:
#Override
public void onViewStateRestored(#Nullable Bundle savedInstanceState) {
if(savedInstanceState!=null){
mRecylcerViewParecelable = savedInstanceState.getParcelable(GRID_LAYOUT_PARCEL_KEY);
}
super.onViewStateRestored(savedInstanceState);
}
onLoadFinished method:
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mMovieAdapter.swapCursor(data);
mGridView.getLayoutManager().onRestoreInstanceState(mRecylcerViewParecelable);}
Try to wrap onRestoreInstanceState() inside Handler:
new Handler().postDelayed(new Runnable() {
#Override public void run() {
mGridView.getLayoutManager().onRestoreInstanceState(listState);
}
}, 300);
It's a hack to delay the onRestoreInstanceState.
I've experienced the same issue, IMO it looks like the RecyclerView keeps going back to initial state because the data in Adapter still being populated when we call the onRestoreInstanceState.

Android exception when popping backstack

This is a followup question to these questions:
popBackStack() after saveInstanceState()
Application crashes in background, when popping a fragment from stack
I am creating an application which uses a service and is reacting to events which are created by the service. One of the events is called within a fragment and is popping from the backstack like this:
getSupportFragmentManager().popBackStack(stringTag, FragmentManager.POP_BACK_STACK_INCLUSIVE);
When the app is in the foreground it works fine. When the app is in the background, I get an
IllegalStateException: Can not perform this action after onSaveInstanceState
I have already tried overriding onSaveInstanceState with an empty method.
Why do I get this exception only when the app is in the background and how can I solve it?
Try something like this.
public abstract class PopActivity extends Activity {
private boolean mVisible;
#Override
public void onResume() {
super.onResume();
mVisible = true;
}
#Override
protected void onPause() {
super.onPause();
mVisible = false;
}
private void popFragment() {
if (!mVisible) {
return;
}
FragmentManager fm = getSupportFragmentManager();
fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}
when you implement the above code alone when you resume the app you will find yourself in a fragment that you actually want to be popped. You can use the following snipped to fix this issue:
public abstract class PopFragment extends Fragment {
private static final String KEY_IS_POPPED = "KEY_IS_POPPED";
private boolean mPopped;
#Override
public void onSaveInstanceState(Bundle outState) {
outState.putBoolean(KEY_IS_POPPED, mPopped);
super.onSaveInstanceState(outState);
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mPopped = savedInstanceState.getBoolean(KEY_IS_POPPED);
}
}
#Override
public void onResume() {
super.onResume();
if (mPopped) {
popFragment();
}
}
protected void popFragment() {
mPopped = true;
// null check and interface check advised
((PopActivity) getActivity()).popFragment();
}
}
Original Author

Facebook FriendPickerFragment showing no friends

Any idea why the list might be empty?
The code is below.
public class PickFBFriendsActivity extends FragmentActivity {
FriendPickerFragment friendPickerFragment;
// A helper to simplify life for callers who want to populate a Bundle with the necessary
// parameters. A more sophisticated Activity might define its own set of parameters; our needs
// are simple, so we just populate what we want to pass to the FriendPickerFragment.
public static void populateParameters(Intent intent, String userId, boolean multiSelect, boolean showTitleBar) {
intent.putExtra(FriendPickerFragment.USER_ID_BUNDLE_KEY, userId);
intent.putExtra(FriendPickerFragment.MULTI_SELECT_BUNDLE_KEY, multiSelect);
intent.putExtra(FriendPickerFragment.SHOW_TITLE_BAR_BUNDLE_KEY, showTitleBar);
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.pick_friends_activity);
FragmentManager fm = getSupportFragmentManager();
if (savedInstanceState == null) {
// First time through, we create our fragment programmatically.
final Bundle args = getIntent().getExtras();
friendPickerFragment = new FriendPickerFragment(args);
friendPickerFragment.setUserId(null);
fm.beginTransaction()
.add(R.id.friend_picker_fragment, friendPickerFragment)
.commit();
} else {
// Subsequent times, our fragment is recreated by the framework and already has saved and
// restored its state, so we don't need to specify args again. (In fact, this might be
// incorrect if the fragment was modified programmatically since it was created.)
friendPickerFragment = (FriendPickerFragment) fm.findFragmentById(R.id.friend_picker_fragment);
}
friendPickerFragment.setOnErrorListener(new PickerFragment.OnErrorListener() {
#Override
public void onError(PickerFragment<?> fragment, FacebookException error) {
PickFBFriendsActivity.this.onError(error);
}
});
friendPickerFragment.setOnDoneButtonClickedListener(new PickerFragment.OnDoneButtonClickedListener() {
#Override
public void onDoneButtonClicked(PickerFragment<?> fragment) {
setResult(RESULT_OK, null);
finish();
}
});
}
private void onError(Exception error) {
String text = getString(R.string.exception, error.getMessage());
Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT);
toast.show();
}
#Override
protected void onStart() {
super.onStart();
}
}
Note that it's pretty much the same as the sample one.
Figured it out: my onStart() method was incomplete, missing the following line:
friendPickerFragment.loadData(false);
Must have deleted it accidently.

Using callbacks in Activity

We have a Webservice class that takes a WebserviceListener as the callback for that specific webservice call. My problem is that Android can recreate my Activity (orientation change, etc...), and then I have the old references in that callback. When I try to set the visibility of a view, or set my hidden flag to vai saveInstanceState method, they are all recreated. How should I address that problem?
The code I have:
public class UploadActivity extends Activity implements {
private Button mButton;
private volatile boolean mHidden;
private class UploadWSListener extends WebServiceAdapter {
#Override
public void onComplete(Bundle bundle) {
mSuggestion = (Suggestion) bundle.getSerializable(WebService.BUNDLE_DATA);
if (mSuggestion.isAutomatic()) {
myHandler.sendEmptyMessage(1);
}
else {
myHandler.sendEmptyMessage(0);
}
}
#Override
public void onServerError(ErrorResult error, Bundle bundle) {
onError(error.getDebugInfo());
}
#Override
public void onError(WebServiceException wse, Bundle bundle) {
onError(wse.getMessage());
}
private void onError(String message) {
Log.w(TAG, "Error downloading suggestions. Error: " + message);
myHandler.sendEmptyMessage(-1);
}
}
private class PhotoUploadHandler extends Handler {
#Override
public void handleMessage(Message msg) {
Log.d(TAG, "Handler is ivoked. What: " + msg.what);
switch (msg.what) {
case 0:
mButton.setText("Upload");
break;
case 1:
mButton.setVisibility(View.GONE);
mHidden = true;
break;
case -1:
Toast.makeText(PhotoUploadActivity.this, "Network error", Toast.LENGTH_LONG).show();
finish();
break;
}
Log.i(TAG, "Handler with " + msg.what + " run.");
}
}
#Override
protected void onSaveInstanceState(Bundle outState) {
Log.i(TAG, "onSaveInstanceState hidden:" + mHidden);
super.onSaveInstanceState(outState);
// outState.putBoolean("hide", mButton.getVisibility() == View.GONE);
outState.putBoolean("hide", mHidden);
}
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
if (savedInstanceState != null && savedInstanceState.getBoolean("hide")) {
mButton.setVisibility(View.GONE);
}
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myHandler = new UploadHandler();
Model model = Model.instance(getApplicationContext());
WebService service = model.getWebService();
if (Model.u) {
Model.u = false;
service.getSuggestions(new UploadWSListener(), Prefs.getUserToken(getApplicationContext()), getLatitude(),getLongitude(), getAltitude());
}
}
}
My main questions:
How can I use this callback design with an Activity? (I'm pretty much stuck with the design)
Is it even smart to hold Views as instance variables in my Activity, if they are recreated unpredictably, or just get them via findViewByID?
I think you need a way to register you activity to and from the listener doing this you can react on orientation changes by unregistering the activity and registering it again when the new activity is created. As far as I know between the destruction and recreation all calls to the "old" activity will be retained till the new activity was created.
Holding views in your activity is ok as they will be released/destructed when your activity gets destroyed. If you have to access them multiple times it's ok to hold them as a class member as getting them via findViewByID() can be expensive when you layout is very deep structured.

Categories

Resources