Geir Smestad Geir Smestad - 3 months ago 74
Android Question

Why does FindViewById return null when switching from portrait to landscape?

The InitView method of one of my activities calls its property

protected ListView MainMenu
{
get
{
return FindViewById<ListView>(Resource.Id.mainmenu);
}
}


...which refers to a ListView defined as part of a RelativeLayout "MainMenuLayout.axml" located in the "layout" folder in my Android project:

<ListView
android:id="@+id/mainmenu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:choiceMode="singleChoice"
android:layout_below="@id/mainmenuHeader"
android:divider="#e5e5e5"
android:dividerHeight="1dp"
android:background="#fff" />


When in Portrait mode, this works as expected. However, when switching to landscape mode, the call to FindViewById above returns null. It is my understanding that Android should use the layout folder for both portrait and landscape mode. Resource.designer.cs also contains

public const int mainmenu = 2131492931;


as expected. Notably, manually handling orientation changes by setting ConfigurationChanges = ConfigChanges.Orientation and ConfigChanges.ScreenSize causes landscape mode to work correctly. Is this a Xamarin issue, or am I misunderstanding something?

Answer

The element with the id "mainmenu" does not exist in the layout that is generated for this activity in portrait mode. FindViewById therefore returns null. You need to ensure that the portrait mode layout contains the "mainmenu" element, or otherwise do a null check, e.g.:

if (MainMenu != null) {
    MainMenu.Visibility = ViewStates.Visible;
}

...before you access the MainMenu property. (Note: Simply checking device orientation before accessing an element that is not present in all layouts can result in subtle bugs, and should not be done).

The long answer to my own question is that this activity does not actually call SetContentView(Resource.Layout.MainMenuLayout) in its InitView, but instead uses SetContentView(Resource.Layout.MasterLayout). So we need to look at MasterLayout.axml, not MainMenuLayout.axml. The kicker is that MasterLayout.axml has two versions, one in the "layout-port" folder and one in "layout". The one in "layout" has the include directive

<include layout="@layout/MainMenuLayout" />

...but the version in "layout-port" does not have this include. This is why the element "mainmenu" only exists in the landscape layout and not in portrait.

Moral of the story: Keep your house in order when using include directives in layouts.

[Edit: To elaborate on the "subtle bugs" note in this response: If you check device orientation with WindowManager.DefaultDisplay.Rotation before accessing an element that only exists in e.g. portrait mode, FindViewById can still return null. On Xamarin, an Activity can be in a condition where device orientation is portrait but the portrait view elements have not yet been created. This can happen if you quickly flip the device immediately after switching Activities. The inconsistent version of the Activity will quickly be stopped and resumed with the correct configuration, but it can persist long enough that your code will execute and you get a NullReferenceException. This is why you should check whether FindViewById returns null, and not just look at device configuration].