Skip to content

Commit b5b5721

Browse files
author
Joe Hansche
committed
Fix race condition caused by delayed hiding of previous state.
The fix for #1 (race condition) also introduces a second race condition when `setState()` is called a second time before the 1st call's previous state view was hidden by the `Runnable`. For example: msv.setState(ContentState.LOADING); // (1) msv.setState(ContentState.CONTENT); // (2) This is the simplest way to reproduce the problem. The default state is `ContentState.CONTENT`. The (1) call shows the `LOADING` view and posts a runnable that intends to hide the `CONTENT` view. Then the (2) call does the opposite: shows the `CONTENT` view and posts a runnable to hide the `LOADING` view. The (2) show happens before the (1) runnable has a chance to hide the `CONTENT` view, so when the runnable finally does get run, it hides the `CONTENT` view, superceding the work done by the (2) call. This change converts the `postRunnable()` behavior into an inner `Handler` subclass that will take a `ContentState` obj that is meant to be hidden. That way when `setState()` is called, we can remove any pending messages that are meant to hide the state that is currently requested to be shown. If however, you are switching states to a different state, then the pending hide will still be executed properly, such as: msv.setState(ContentState.LOADING); msv.setState(ContentState.GENERAL_ERROR); // will still hide CONTENT *and* will hide LOADING This manifests easily when you set up your view to have an initial state of `LOADING` in `onViewCreated()`, and in `onViewStateRestored()`, you set the state back to `CONTENT`.
1 parent 790f66f commit b5b5721

1 file changed

Lines changed: 51 additions & 13 deletions

File tree

library/src/com/meetme/android/multistateview/MultiStateView.java

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import android.content.Context;
44
import android.content.res.TypedArray;
5+
import android.os.Handler;
6+
import android.os.Looper;
7+
import android.os.Message;
58
import android.os.Parcel;
69
import android.os.Parcelable;
710
import android.util.AttributeSet;
8-
import android.util.Log;
911
import android.util.SparseArray;
1012
import android.view.View;
1113
import android.widget.FrameLayout;
@@ -23,15 +25,14 @@ public class MultiStateView extends FrameLayout {
2325
private View mNetworkErrorView;
2426
private View mGeneralErrorView;
2527
private OnClickListener mTapToRetryClickListener;
28+
private MultiStateHandler mHandler;
2629

2730
public MultiStateView(Context context) {
28-
super(context);
29-
parseAttrs(context, null);
31+
this(context, null);
3032
}
3133

3234
public MultiStateView(Context context, AttributeSet attrs) {
33-
super(context, attrs);
34-
parseAttrs(context, attrs);
35+
this(context, attrs, 0);
3536
}
3637

3738
public MultiStateView(Context context, AttributeSet attrs, int defStyle) {
@@ -175,15 +176,10 @@ public void setState(final ContentState state) {
175176

176177
final ContentState previousState = mViewState.state;
177178

179+
// Remove any previously pending hide events for the to-be-shown state
180+
mHandler.removeMessages(MultiStateHandler.MESSAGE_HIDE_PREVIOUS, state);
178181
// Only change visibility after other UI tasks have been performed
179-
this.post(new Runnable() {
180-
@Override
181-
public void run() {
182-
Log.v("DALLAS", "Attempting to change state from " + previousState);
183-
// Hide the current state
184-
getStateView(previousState).setVisibility(View.GONE);
185-
}
186-
});
182+
mHandler.sendMessage(mHandler.obtainMessage(MultiStateHandler.MESSAGE_HIDE_PREVIOUS, previousState));
187183

188184
mViewState.state = state;
189185

@@ -363,6 +359,19 @@ private void setViewState(MultiStateViewData state) {
363359
setCustomErrorString(state.customErrorString);
364360
}
365361

362+
@Override
363+
protected void onAttachedToWindow() {
364+
super.onAttachedToWindow();
365+
mHandler = new MultiStateHandler(getHandler().getLooper());
366+
}
367+
368+
@Override
369+
protected void onDetachedFromWindow() {
370+
mHandler.removeMessages(MultiStateHandler.MESSAGE_HIDE_PREVIOUS);
371+
mHandler = null;
372+
super.onDetachedFromWindow();
373+
}
374+
366375
@Override
367376
public void addView(View child) {
368377
if (!isViewInternal(child)) {
@@ -538,4 +547,33 @@ public MultiStateViewData[] newArray(int size) {
538547
}
539548
};
540549
}
550+
551+
/**
552+
* Handler used to hide the previous state when switching to a new state
553+
*
554+
* @author jhansche
555+
*/
556+
private class MultiStateHandler extends Handler {
557+
public static final int MESSAGE_HIDE_PREVIOUS = 0;
558+
559+
@SuppressWarnings("unused")
560+
public MultiStateHandler() {
561+
super();
562+
}
563+
564+
public MultiStateHandler(Looper looper) {
565+
super(looper);
566+
}
567+
568+
@Override
569+
public void handleMessage(Message msg) {
570+
switch (msg.what) {
571+
case MESSAGE_HIDE_PREVIOUS:
572+
ContentState previousState = (ContentState) msg.obj;
573+
View previousView = getStateView(previousState);
574+
if (previousView != null) previousView.setVisibility(View.GONE);
575+
break;
576+
}
577+
}
578+
}
541579
}

0 commit comments

Comments
 (0)