0

I would like to tag this question with both C# and Java; for Java programmers, I'm sure you can understand the C# code here easily, they are very similar.

I've encountered many weird problems since knowing how to program and this is one of the weirdest problems, it is surely a problem of the Android engine and API.

Firstly I would like to shortly describe how strange it is. Can you think of any situation in which the call setContentView does not do anything? No exception is thrown, the content view of the Activity is simply not changed to what I want (even passing a null value makes nothing happen).

In fact I've not tried testing some other scenarios to reproduce the problem. But the scenario I'm going to describe here should reproduce the problem easily.

You need a simple custom Fragment where you load a simple layout having a simple Button. Now in the onCreateView callback, you inflate the layout, hookup the Click event of the Button to fire another exposed event via the custom Fragment.

The Fragment has RetainInstance set to true. Now in the MainActivity, you declare a static field of the custom Fragment. The Fragment is created only once (by checking against null) in the OnCreate callback of the MainActivity. We hook up the exposed click event of the custom Fragment with a handler which simply shows the new content using SetContentView.

The code works fine without screen rotation first. But after screen rotation, the SetContentView simply does nothing and there is no exception. It sounds like the click event is not fired due to something BUT in fact it's still fired OK and the SetContentView is actually called (I'm sure about this, because I debugged it by setting a breakpoint there).

Here is the code:

The main layout: (this is named as Main.axml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:id="@+id/container"
   android:orientation="vertical"
   android:layout_width="match_parent"
   android:layout_height="match_parent">
</LinearLayout>

The custom Fragment:

public class CustomFragment : Fragment {
    //the click event
    public event EventHandler ButtonClick;
    void onButtonClick(EventArgs e){
       var handler = ButtonClick;
       if(handler != null) handler(this,e);
    }
    public override View OnCreateView(LayoutInflater inflater, ViewGroup parent, 
                                                               Bundle savedInstanceState){
        //we can even use a simple layout as a Button.
        var button = new Button(parent.Context);
        button.Click += (s,e) => {
            onButtonClick(EventArgs.Empty);
        };
        return button;
    }
}

The MainActivity:

[Activity(Label = "Hello", MainLauncher = true]
public class MainActivity : AppCompatActivity {
    //the static custom Fragment
    static CustomFragment frag;
    protected override void OnCreate(Bundle bundle){
       base.OnCreate(bundle);
       SetContentView(Resource.Layout.Main);
       //this ensures that the code inside if block is run just once 
       //between screen rotations
       if(frag == null){
           frag = new CustomFragment(this);
           frag.RetainInstance = true;
           frag.ButtonClick += (s,e) => {
                //whatEverView here is any View, layout resource id 
                //or even null value.
                SetContentView(whatEverView);
           };
           //load the fragment into the container in the Main layout
           var ft = FragmentManager.BeginTransaction();
           ft.Replace(Resource.Id.container, frag);
           ft.Commit();
       }
    }
}

Well that's all setup to reproduce the strange problem. As I described above, the code runs just fine without rotating the screen. But after rotating the screen, SetContentView does nothing although it is surely called (I know for sure by setting break point on the call).

One more thing which I think is also important is all calls after SetContentView seem not to be called (because the break points set on them are not reached, after SetContentView run, it's like that there was a return interrupting the code there). No exception and the app is not crashed, just runs normally after that (clicking the Button again will again call the SetContentView).

It's very weird and there must be something wrong internally happening that I've not known of. Reading the source code is not my strength partly because I'm still fairly new to Android programming.

You may need to try replicating the problem on your own machine to understand this issue.

Finally the code is run on Kitkat and Marshmallow Android OS (run by Visual Studio Emulator).

halfer
  • 19,824
  • 17
  • 99
  • 186
Hopeless
  • 4,397
  • 5
  • 37
  • 64

2 Answers2

1

1) When the Activity is recreated, you are losing scope of the original activity (this) in the implied this.SetContentView(whatEverView);.

2) In your Fragment's OnCreateView, you are internally using the Context of the ViewGroup which is a MainActivity instance, but this will be changing every time the Activity is recreated due to a configuration change. OnAttach and OnDetach should be used to assign the current Context/Activity and to clear/null it.

3) Instead of using a static var for your CustomFragment, the FragmentManager.FindFragmentByTag should used and the fragment recreated as needed as Fragments can be reclaimed by the OS even if it is set to be retained, you will be chasing phantom crashes in the wild...

This is a quick fix example, the internals of CustomFragment still need to reset/clear the old event handlers when the fragment is detached from the Activity and correct the Button's use of the old/new context.

public class MainActivity : AppCompatActivity
{
    const string customFragmentTag = "custom_fragment";
    CustomFragment frag;
    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);
        SetContentView(Resource.Layout.Main);

        frag = FragmentManager.FindFragmentByTag(customFragmentTag) as CustomFragment;
        if (frag == null)
        {
            frag = new CustomFragment(Resource.Layout.Frag);
            FragmentManager.BeginTransaction().Add(Resource.Id.container, frag, customFragmentTag).Commit();
        }
        frag.ButtonClick += (s, e) => SetContentView(Resource.Layout.Frag);
    }
}

If your Fragment needs access to the current Context/Activity, you should implemented the following overrides (currentActivity is a class level Activity var):

public override void OnAttach(Activity activity)
{
    base.OnAttach(activity);
    currentActivity = activity;
}

public override void OnAttach(Android.Content.Context context)
{
    base.OnAttach(context);
    currentActivity = context as Activity;
}

public override void OnDetach()
{
    base.OnDetach();
    currentActivity = null;
}
SushiHangover
  • 73,120
  • 10
  • 106
  • 165
  • Thank you, the answer helped me a lot to solve my problem. However I don't understand what you said in `3)`, is there anything wrong with using static field in this case? Also the emphasized phrase ***can be reclaimed*** means the `Fragment` can be recreated regardless of the `RetainInstance` set to `true`, I'm not sure if you mean that even when the `Fragment` is declared as static? Again thank you for your great answer. – Hopeless Sep 07 '16 at 07:26
  • @Hopeless Fragments, retained or not, can be reclaimed by the OS no matter if you are assigning them to a C# static var or not. Holding onto a Fragment via a static var means nothing since you really need to retrieve the Fragment from the FragmentManager. If the OS needs the memory, say the user get a call, or opens the camera, ... when they return to your activity the Fragments might have been destroyed. See the second point in this answer: `Will the fragment be destroyed when the user leaves the activity?` http://stackoverflow.com/a/11318942/4984832 – SushiHangover Sep 07 '16 at 07:35
  • yes that makes sense. That means we cannot simply keep the Fragment in such cases. I understand that you suggested me to use `FindFragmentByTag` instead of a static field, that's why I raised a question about static field (at least I found that it's a way to retain data in Activity through its life cycles). – Hopeless Sep 07 '16 at 07:58
  • @Hopeless *Personally*, I avoid static vars like the plague in mobile development in dealing with the lifecycle of JNI-wrapped objects assigned to C# static vars, you end up chasing your tail as the reports from the field just say "your app crashes at random times"... Even you are using crash reporting to post the prior crash to a crash service, that means your user has to have a net connection on app start and app startup times are longer and all you end up with is a null exception report and a user that post a negative Store review... – SushiHangover Sep 07 '16 at 08:22
  • 1
    @Hopeless I treat everything as transient and have not seen a native crash due to a null exception in Android/iOS for over two years... Other crashes yes, but not one due to a lifecycle issue. So in your case, I would use a retained fragment as if it is still in memory it is faster to reattach to the parent and any tasks/threads within it are still running, but alway create the fragment with a tag, and pull it back from the FragmentManager by tag and it does not exist, create a new one again... – SushiHangover Sep 07 '16 at 08:23
-2

First set TAG to your fragment at the time of replacing .then on back press You can use Find Fragment By Tag and do what you want.

Parvesh Khan
  • 1,356
  • 1
  • 10
  • 9