DEV Community

Emmanuel Guerra
Emmanuel Guerra

Posted on

Handle your Android app orientation changes in semi-automatic

TLDR: Using the power of a shared ViewModel + LiveData (and some debugging to see what is happening) you could listen to orientation changes in any activity/fragment by overriding the onConfigurationChanged function and observing the new Configuration values.

When developing an Android application, most teams point to a portrait-only experience, which is good if it fills your needs, but it often limits the user experience and will result of users uninstalling the app.

Today, I will show you how to manage configuration changes in a reactive way and from both sides: the sensor and from a user interaction.

The example comes from a pet project I have been working: An app to see Twitch clips from a streamer and download them.
First, let’s see why I needed to listen to orientation changes:

  1. Change the orientation if the user pressed a button.
  2. Update the player’s UI when the orientation changed.
  3. If leaving the player, the user’s orientation must remain the same it had previously.

The 1st point was easy to do: The Android API provides a function to change the orientation:

activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
Enter fullscreen mode Exit fullscreen mode

And that is. The screen orientation changes, and the app will be in landscape mode.

The second, however, was trickier since I needed a variable that tell me the current orientation and update the UI in base of that.

My first approach was to have a variable attached to my fragment. It failed because the user can enter either on landscape or in portrait. So, at start, I needed to know the current configuration.

I started to dig into the ActivityInfo code to see how many orientations are. Turns out there are 17 different orientations. Four of them contains the name PORTRAIT, so we will start from that. Let’s define an extension function that check is the current activity is in portrait:

fun Activity.isInPortrait() = requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT ||
        requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT ||
        requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT ||
        requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
Enter fullscreen mode Exit fullscreen mode

However, I was missing one definition that is the default orientation when you have not locked your screen: UNSPECIFIED. This is the default orientation of activities if in the manifest we define the configChanges value and add the orientation specification.

This specify the system that we will manually take care of configuration changes. I am using ViewModels and Room so there is no problem with doing it by myself.
On a side note, this also let’s ExoPlayer to behave like in Youtube app: The video does not stop if your orientation changes.

Now, our function we look like this:

fun Activity.isInPortrait() = requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT ||
        requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT ||
        requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT ||
        requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT ||
        requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED //The new orientation
Enter fullscreen mode Exit fullscreen mode

Now the UI updates properly if we enter on PORTRAIT, LANDSCAPE and if an user triggers the full screen mode.

But we still lack a way of knowing when the configuration changes when the devices is rotated with the sensor.

After digging through StackOverflow and Github, I knew about the onConfigurationChanged method. It’s an overridable method of the Activities that receives a Configuration object as a parameter. The configuration object comes with a orientation variable that is an interger with 3 possible values:

UNSPECIFIED, LANDSCAPE, PORTRAIT. This is exactly what I need!

I define a simple ViewModel which would update the Configuration object with MutableLiveData. The ViewModel will be created lazily on the activity, and be shared across fragments inside that activity:

class OrientationViewModel: BaseViewModel() {

    private val _configData = MutableLiveData<Configuration>()
    val configData = _configData as LiveData<Configuration>

    fun changeConfiguration(configuration: Configuration?){
        _configData.value = configuration
        Timber.d("New configuration. Orientation: ${configuration?.orientation}")
    }

}
Enter fullscreen mode Exit fullscreen mode

Override the onConfigurationChanged method on my HostActivity:

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        orientationViewModel.changeConfiguration(newConfig)
    }
Enter fullscreen mode Exit fullscreen mode

Then, on my fragment that holds the Player, observe the changes:

orientationViewModel.configData.observe(viewLifecycleOwner, Observer { config ->
            if(config != null){
                updatePlayerView(fullScreenBtn, config.orientation)
                updateWindowMode(config.orientation)
            }
        })
Enter fullscreen mode Exit fullscreen mode

Done! If I set a new orientation with activity?.requestOrientation or the user use the sensor to change to landscape/portrait, the activity will handle the UI changes.

As for the last problem, a simple line inside the Fragment's onDetach function will leave the user in their default orientation:

requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
Enter fullscreen mode Exit fullscreen mode

That’s it! You could check the full project in Github:

GitHub logo eagskunst / VideoWorld

Repository that contains my tests with Exoplayer and network videos APIs.

If you are interested on how to make a custom ExoPlayer controls view, how to download files using WorkManager, building recycler views with Epoxy or anything related to the project, like this article and leave a comment with what you want see next!

Top comments (0)