Adaptive Layouts

1. Before you begin

Android devices come in a variety of shapes, sizes, and form factors. You should design your app to run on these different types of devices, from small screen devices to larger screen devices. Developers writing production-ready apps may support Android Wear, Android Auto, and Android TV, but these topics are outside the scope of this course. When your app supports the widest variety of screens, you can make it available to the greatest number of users with different devices.

Your app must have a flexible layout. Instead of defining your layout with rigid dimensions that assume a certain aspect ratio and screen size, your layout should be able to gracefully adapt to various screen sizes and orientations. The same principle applies when your app is running on a foldable device where the screen size and aspect ratio may change while your app is running. At the end of this codelab, you will learn a brief introduction to foldable devices.

aecb59fc49fb4abf.png

Prerequisites

  • How to download code into Android Studio and run it.
  • Familiar with the Android architecture components ViewModel, and LiveData.
  • Basic knowledge of Navigation components.

What you'll learn

  • How to add SlidingPaneLayout to your app.

What you'll build

  • Update the Sports app to adapt to large screens.

What you'll need

  • A computer with Android Studio installed.
  • Starter code for the Sports app.

Download the starter code for this codelab

This codelab provides starter code for you to extend with features taught in this codelab. Starter code may contain code that is familiar to you from previous codelabs, and also code that is unfamiliar to you that you will learn about in later codelabs.

To get the code for this codelab from GitHub and open it in Android Studio, do the following.

  1. Start Android Studio.
  2. On the Welcome to Android Studio window, click Check out project from Version Control.
  3. Choose Git.

b89a22e2d8cf3b4e.png

  1. In the Clone Repository dialog, paste the provided code URL into the URL box.
  2. Click the Test button, wait, and make sure there is a green popup bubble that says Connection successful.
  3. Optionally, change the Directory to something different than the suggested default.

e4fb01c402e47bb3.png

  1. Click Clone. Android Studio starts fetching your code.
  2. In the Checkout from Version Control popup, click Yes.

1902d34f29119530.png

  1. Wait for Android Studio to open.
  2. Select the correct module for your codelab starter or solution code.

2371589274bce21c.png

  1. Click the Run button 11c34fc5e516fb1c.png to build and run your code.

2. Starter app overview

Sports app consists of two screens. The first screen displays the sports list. The user can select a particular sport item and the second screen is displayed. The second screen is a details screen that displays the selected sports news. The details screen displays placeholder text to simplify implementation.

Starter code walk through

The starter code you downloaded has the list screen and the details screen layouts pre-designed for you. In this pathway, you will focus only on making your app adapt to large screens. You will use SlidingPaneLayout to take advantage of the large screen. Here is a brief walkthrough of some of the files to get you started.

fragment_sports_list.xml

  • Open res/layout/fragment_sports_list.xml in Design view.
  • This contains the layout of the first screen in your app that is the sports list.
  • This layout consists of a Recyclerview that shows a list of sports news.

19347945e512f94f.png

d9af155f87ddbcdf.png

sports_list_item.xml

  • Open res/layout/sports_list_item.xml in Design view.
  • This contains the layout of each item in the Recyclerview.
  • This layout consists of a thumbnail image of the sport, the News title, and a placeholder text for a brief sports news.

afe02fbb229608c2.png

fragment_sports_news.xml

  • Open res/layout/fragment_sports_news.xml in Design view.
  • This contains the layout of the second screen in your app. This screen is displayed when the user selects a sport from the Recyclerview.
  • This layout consists of a sport image banner, and placeholder text for the sport news.

f21a8edd59a53233.png

main_activity.xml and content_main.xml

These two define the main activity layout with a single fragment.

The navigation graph contains two destinations, one for sports lists and one for sports news.

res/values folder

You are familiar with the resource files in this folder.

  • colors.xml contains the theme colors used in the app.
  • strings.xml contains all the strings your app needs.
  • themes.xml contain the UI customization done for your app.

MainActivity.kt

This contains the default template generated code to set the activity's content view as main_activity.xml. The method onSupportNavigateUp() is overridden to handle the default Up navigation from the app bar.

model/Sport.kt

This is a data class which holds data to be displayed in each row of the sports list Recyclerview.

data/SportsData.kt

This file contains a function called getSportsData() which returns an ArrayList pre-populated with hardcoded sports data.

SportsViewModel.kt

This is the shared ViewModel for the app. The ViewModel is shared by SportsListFragment, the first screen with the sports list, and NewsDetailsFragment, the second screen with detailed sports news.

  • The _currentSport property is of the type MutableLiveData, which stores the current sport selected by the user. The property currentSport is the backing property for _currentSport, and exposed as the public read-only version for other classes.
  • The _sportsData property contains the list of sports data. Similar to the previous property, sportsData is the public read-only version for this property.
  • The initializer init{} block initializes _currentSport and _sportsData. The _sportsData is initialized with the entire list of sports from the data/SportsData.kt. The _currentSport is initialized with the first item in the list.
  • The function updateCurrentSport() takes in a Sports instance and updates _currentSport with the passed in value.

SportsAdapter.kt

This is the adapter for the RecyclerView. In the constructor, the click listener is passed in. Most of the code in this file is boilerplate code you're familiar with from previous codelabs.

SportsListFragment.kt

This is the first screen fragment, where the sports list is displayed.

  • onCreateView() function inflates the fragment_sports_list layout XML using the binding object.
  • onViewCreated() function sets up the RecyclerView adapter. It updates the user selected sport as the current sport in the shared ViewModel, the SportsViewModel. It navigates to the details screen with sports news and submits the sports list to the adapter to be displayed using submitList(List).

NewsDetailsFragment.kt

This is the second screen in your app, where placeholder text for the sports news is displayed.

  • onCreateView() function inflates the fragment_sports_news layout XML using the binding object.
  • onViewCreated() function attaches an observer on the SportsViewModel's property, currentSport to update the UI automatically when the data changes. Inside the observer, the sports title, image and news are updated.

Build and run the app

  1. Build and run the app on an emulator or device. Select any item from the sports list, and the app should navigate to the second screen with placeholder text for news.

3. List-Detail pattern

The current starter app fails to take full advantage of screen real estate on larger devices like tablets. In order to resolve this issue, you will display the app UI using the List-Detail pattern, which you will learn in this codelab.

Run app on a tablet

In this task, you will create an emulator with a tablet profile. Once the emulator has been created, you will run the sports app starter code and observe the UI.

  1. In Android Studio, go to Tools > AVD Manager.
  2. The Android Virtual Device Manager window will be displayed. Click + Create New Virtual Device... displayed at the bottom.
  3. The Virtual Device Configuration window is displayed. Here, you will configure the emulator hardware and OS. Click Tablet in the left pane. Select Pixel C or any other similar hardware profile in the middle pane.

5c4600c27a47077e.png

  1. Click Next.
  2. Select the latest system image, at the time of writing this codelab the latest is R (API level 30).
  3. Click Next.
  4. You can rename the virtual device now, this is optional.
  5. Click Finish.
  6. You will be navigated back to the Android Virtual Device Manager window. Click on the launch icon 65b3080672a3f31.pngnext to the newly created virtual device.
  7. The emulator with tablet profile should launch. Please be patient, this may take some time.
  8. Close the Android Virtual Device Manager window.
  9. Run the sports app on your newly created emulator.

200e209de7a2f0ad.png

Notice on large devices, the app does not utilize the entire screen. The list-detail is more efficient on a large screen than a list. An item-detail pattern, also called a master-detail pattern, shows a list of items on one side of the layout, and the detail is shown next to it when you tap an item. Usually, these views are displayed only on large screens such as tablets, since they have more room to display more content.

The following images are an example of a list-detail pattern:

9ebca0de8956275a.png

The above list-detail patterns display a list of items on the left and details of the selected item on the right.

In the same way, if you use the above pattern in your sports app, the news fragment will be your details screen.

51c9542717d2f875.png

In this codelab, you will learn how to implement the list-detail UI using SlidingPaneLayout.

4. SlidingPaneLayout pattern

A list-detail UI may need to behave differently depending on the screen size. On large displays, there is ample room to have the list and the detail panes side-by-side. Clicking on a list item displays its details in the detail pane. However, on small screens, these appear crowded. Instead of displaying both panes at once, it's better to display one at a time. Initially, the list pane fills the screen. Tapping an item replaces the list pane with the detail pane for that item, which also fills the screen.

You will learn how to use a SlidingPaneLayout to manage the logic for selecting the appropriate user experience based on the current screen size.

b0a205de3494e95d.gif

Notice how the details pane slides over the list pane on smaller screens.

Below are images that illustrate how the SlidingPaneLayout appears on a smaller screen. Observe how the details pane overlaps with the list pane when an item from the list is selected. So both panes are always present!

1363b67d106ea395.png

c64fa6a0641320dd.png

Therefore, the SlidingPaneLayout supports showing two panes side by side on larger devices, while automatically adapting to show only one pane at a time on smaller devices such as phones.

5. Add library dependencies

  1. Open build.gradle (Module: Sports.app).
  2. In the dependencies section, include the following dependency to use SlidingPaneLayout in your app.
dependencies {
...
    implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0-beta01"
}

6. Configure sports list fragment xml

In this task, you convert the root layout of fragment_sports_list to SlidingPaneLayout. As you have already learned, the SlidingPaneLayout provides a horizontal, two pane layout for use at the top level of a UI. This layout uses the first pane as a content list or a browser, subordinate to a primary detail view for displaying content in the other pane.

In the Sports app, the first pane will be the RecyclerView displaying the sports list and the second pane displays sports news.

Add SlidingPaneLayout

  1. Open fragment_sports_list.xml. Notice that the root layout is a FrameLayout.
  2. Change the FrameLayout to androidx.slidingpanelayout.widget.SlidingPaneLayout.
<androidx.slidingpanelayout.widget.SlidingPaneLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".SportsListFragment">

   <androidx.recyclerview.widget.RecyclerView...>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
  1. Add an android:id attribute to the SlidingPaneLayout and give it a value of @+id/sliding_pane_layout.
<androidx.slidingpanelayout.widget.SlidingPaneLayout
   ...
   android:id="@+id/sliding_pane_layout"
   ...>

Add a second pane to SlidingPaneLayout

In this task, you will add a second child to the SlidingPaneLayout. This will be displayed as the right content pane.

  1. In fragment_sports_list.xml, below the RecyclerView, add a second child, androidx.fragment.app.FragmentContainerView.
  2. Add the required attributes, layout_height and layout_width to the FragmentContainerView. Give them a value of match_parent. Note that you will update these values later.
<androidx.fragment.app.FragmentContainerView
   android:layout_height="match_parent"
   android:layout_width="match_parent"/>
  1. Add an android:id attribute to the FragmentContainerView and give it a value of @+id/detail_container.
android:id="@+id/detail_container"
  1. Add NewsDetailsFragment to the FragmentContainerView by using the android:name attribute.
android:name="com.example.android.sports.NewsDetailsFragment"

Update the layout_width attribute

The SlidingPaneLayout uses the width of the two panes to determine whether to show the panes side by side. For example, if the list pane is measured to have a minimum size of 300dp and the detail pane needs 400dp, then the SlidingPaneLayout automatically shows the two panes side by side as long as it has at least 700dp of width available.

Child views overlap if their combined width exceeds the available width in the SlidingPaneLayout. In this case, the child views expand to fill the available width in the SlidingPaneLayout.

To determine the width of the child views, you should have some basic information on device screen widths. The following table shows the list of opinionated breakpoints for you to design, develop, and test against resizable application layouts. They have been chosen specifically to balance layout simplicity with the flexibility to optimize your app for unique cases.

Width

Breakpoint

Device representation

Compact width

< 600dp

99.96% of phones in portrait

Medium width

600dp+

93.73% of tablets in portraitLarge unfolded inner displays in portrait

Expanded width

840dp+

97.22% of tablets in landscapeLarge unfolded inner displays in landscape

f9a18939802543dd.png

In the Sports app, you want to display a single pane, the sports list on phones, that is for devices with a width less than 600dp. To display both panes on tablets, the combined width should be greater than 840dp. You can use a width of 550dp for the first child, the recycler view and 300dp for the second child, the FragmentContainerView.

  1. In fragment_sports_list.xml, change the layout width of the RecyclerView to 550dp and that of the FragmentContainerView to 300dp.
<androidx.recyclerview.widget.RecyclerView
   ...
   android:layout_width="550dp"
   .../>

<androidx.fragment.app.FragmentContainerView
   ...
   android:layout_width="300dp"
   .../>
  1. Run the app on the emulator with the tablet profile, and an emulator with the phone profile.

ad148a96d7487e66.png

Notice that two panes are displayed on the tablet. You'll fix the width of the second pane on the tablet in the later step.

  1. Run the app on the emulator with the phone profile.

a6be6d199d2975ac.png

Add layout_weight

In this task, you will fix the UI on the tablet and make the second pane take up the entire remaining space.

The SlidingPaneLayout supports defining how leftover space is divided after measurement using the layout parameter layout_weight on child views if the views do not overlap. This parameter applies only to width.

  1. In fragment_sports_list.xml, add layout_weight to FragmentContainerView and give it a value of 1. Now, the second pane expands to fill the space remaining after the list pane is measured.
android:layout_weight="1"
  1. Run the app.

ce3a93fe501ee5dc.png

Congratulations! You have successfully added the SlidingPaneLayout. You aren't done yet. You have to implement the back navigation and update the second pane when an item is selected from the list. You will implement these in a later task.

7. Swap out the details pane

Run the app on the emulator with the tablet profile. Select a list item from the sports list. Notice that the app navigates to the details pane.

8fedee8d4837909.png

In this task, you will fix this issue. Currently the dual pane content is being updated with the selected sport and then the app navigates to the NewsDetailsFragment.

  1. In the SportsListFragment file, in the function onViewCreated(), locate the following lines, which navigate to the details screen.
// Navigate to the details screen
val action = SportsListFragmentDirections.actionSportsListFragmentToNewsFragment()
this.findNavController().navigate(action)
  1. Replace the above lines with the following code:
binding.slidingPaneLayout.openPane()

Call openPane() on the SlidingPaneLayout to swap the second pane over the first pane. This will not have any visible effect if both panes are visible such as on a tablet.

  1. Run the app on the tablet and phone emulator. Notice the dual pane content is being updated properly.

b0d3c8c263be15f8.png

In your next task, the next functionality you will add to the app is custom back navigation.

8. Add custom back navigation

For smaller devices where the list and detail panes overlap, you should ensure that the system back button takes the user back from the detail pane back to the list pane. You can do this by providing custom back navigation and connecting an OnBackPressedCallback to the current state of the SlidingPaneLayout.

Back navigation

Back navigation is how users move backward through the history of screens they previously visited. All Android devices provide a Back button for this type of navigation. Depending on the user's Android device, this button might be a physical button or a software button.

Custom back navigation

Android maintains a back stack of destinations as the user navigates throughout your application. This usually allows Android to properly navigate to previous destinations when the Back button is pressed. However, there are a few cases where your app might need to implement its own Back behavior in order to provide the best possible user experience.

For example, when using a WebView like a Chrome browser, you might want to override the default Back button behavior to allow the user to navigate back through their web browsing history instead of the previous screens in your app.

Similarly, you need to provide a custom back navigation to the SlidingPaneLayout and navigate the app from the detail pane back to the list pane.

Implement custom back navigation

To implement the custom back navigation in your Sports app, you need to:

  • Define a custom callback to handle the back key press, which overrides OnBackPressedCallback.
  • Register and add the callback instance.

First, define the custom callback.

  1. In the SportsListFragment file, add a new class below the SportsListFragment class definition. Name it SportsListOnBackPressedCallback.
  2. Pass in a private instance of SlidingPaneLayout as a constructor parameter.
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
)
  1. Extend the class from OnBackPressedCallback. The OnBackPressedCallback class handles onBackPressed callbacks. You will fix the constructor parameter error shortly.
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback()

The constructor for OnBackPressedCallback takes a boolean for the initial enabled state. Only when a callback is enabled (i.e., isEnabled() returns true) will the dispatcher call the callback's handleOnBackPressed() to handle the Back button event.

  1. Pass in slidingPaneLayout.isSlideable* && slidingPaneLayout.isOpen* as constructor parameter to OnBackPressedCallback. The boolean isSlideable will only be true if the second pane is slidable, which would be on a smaller screen and a single pane is being displayed. The value of isOpen will be true if the second pane - the contents pane is completely open.
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen)

This code will ensure that the callback is only enabled on the smaller screen devices and when the content pane is open.

  1. To fix the error about the unimplemented method, click on the red bulb cb1d366f3ceb9ad5.png and select Implement members.
  2. Click ok in the Implement members popup to override the handleOnBackPressed method.

Your class should look like this:

class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen) {
   /**
    * Callback for handling the [OnBackPressedDispatcher.onBackPressed] event.
    */
   override fun handleOnBackPressed() {
       TODO("Not yet implemented")
   }
}
  1. Inside the handleOnBackPressed() function, delete the TODO statement and add the following code to close the content pane and return to the list pane.
slidingPaneLayout.closePane()

Monitor SlidingPaneLayout's events

In addition to handling back press events, you must listen and monitor events related to the sliding pane. As the content pane slides, the callback should be enabled or disabled accordingly. You will use PanelSlideListener to do this.

The interface SlidingPaneLayout.PanelSlideListener contains three abstract methods onPanelSlide(), onPanelOpened(), and onPanelClosed(). These methods are called when the details pane slides, opens, and closes.

  1. Extend the SportsListOnBackPressedCallback class from SlidingPaneLayout.PanelSlideListener.
  2. To resolve the error, implement the three methods. Click on the red bulb and select Implement members in the Android Studio.

bddae0e597f6e1d3.png

  1. Your SportsListOnBackPressedCallback class should look similar to the following:
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen),
  SlidingPaneLayout.PanelSlideListener{

   override fun handleOnBackPressed() {
       slidingPaneLayout.closePane()
   }

   override fun onPanelSlide(panel: View, slideOffset: Float) {
       TODO("Not yet implemented")
   }

   override fun onPanelOpened(panel: View) {
       TODO("Not yet implemented")
   }

   override fun onPanelClosed(panel: View) {
       TODO("Not yet implemented")
   }
}
  1. Remove the TODO statements.
  2. Enable the OnBackPressedCallback callback, when the details pane is opened (is visible) . This can be achieved by making a call to setEnabled() function and passing in true. Write the following code inside onPanelOpened():
setEnabled(true)
  1. The above code can be simplified using the property access syntax.
override fun onPanelOpened(panel: View) {
   isEnabled = true
}
  1. Similarly set isEnabled to false, when the details pane is closed.
override fun onPanelClosed(panel: View) {
   isEnabled = false
}
  1. The final step in completing the callback is to add the SportsListOnBackPressedCallback listener class to the list of listeners that will be notified of the details pane slide events. Add an init block to the SportsListOnBackPressedCallback class. Inside the init block, make a call to slidingPaneLayout.addPanelSlideListener() passing in this.
init {
   slidingPaneLayout.addPanelSlideListener(this)
}

The completed SportsListOnBackPressedCallback class should look similar to the following:

class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen),
  SlidingPaneLayout.PanelSlideListener{

   init {
       slidingPaneLayout.addPanelSlideListener(this)
   }

   override fun handleOnBackPressed() {
       slidingPaneLayout.closePane()
   }

   override fun onPanelSlide(panel: View, slideOffset: Float) {
   }

   override fun onPanelOpened(panel: View) {
       isEnabled = true
   }

   override fun onPanelClosed(panel: View) {
       isEnabled = false
   }
}

Register the callback

To see your callback in action, register the callback using the dispatcher, OnBackPressedDispatcher.

The base class for FragmentActivity, allows you to control the behavior of the Back button by using its OnBackPressedDispatcher. The OnBackPressedDispatcher controls how Back button events are dispatched to one or more OnBackPressedCallback objects.

Add the callback via the addCallback() method. This method takes a LifecycleOwner. This ensures that the OnBackPressedCallback is only added when the LifecycleOwner is Lifecycle.State.STARTED. The activity or fragment also removes registered callbacks when their associated LifecycleOwner is destroyed, which prevents memory leaks and makes it suitable for use in fragments or other lifecycle owners that have a shorter lifetime.

The addCallback() method also takes in the instance the callback class as the second parameter. You will register the callback using the following steps:

  1. In the SportsListFragment file, inside the function onViewCreated(), just below the declaration of the binding variable, create an instance for the SlidingPaneLayout and assign the value of binding.slidingPaneLayout to it.
val slidingPaneLayout = binding.slidingPaneLayout
  1. In the SportsListFragment file, inside the function onViewCreated(), just below the declaration of the slidingPaneLayout, add the following code:
// Connect the SlidingPaneLayout to the system back button.
requireActivity().onBackPressedDispatcher.addCallback(
   viewLifecycleOwner,
   SportsListOnBackPressedCallback(slidingPaneLayout)
)

The above code uses addCallback(), passing in the viewLifecycleOwner and an instance of SportsListOnBackPressedCallback. This callback is only active during the fragment's life cycle.

  1. It's time to run the app on an emulator with a phone profile and see your custom back button functionality in action.

33967fa8fde5b902.gif

9. Lock mode

When the list and detail panes overlap on smaller screens like phones, users can swipe in both directions by default, freely switching between the two panes even when not using gesture navigation. You can lock or unlock the details pane by setting the lock mode of the SlidingPaneLayout.

  1. In the emulator with the phone profile, try to swipe the details pane off the screen.
  2. You can also swipe in the details pane, try this on your own.
  3. This is not a desirable feature in your Sports app. It's a good idea to lock the SlidingPaneLayout in order to prevent users from swiping in and out using gestures. To implement this, in the onViewCreated() method, below the slidingPaneLayout definition, set the lockMode to LOCK_MODE_LOCKED:
slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

To learn more about the other lock modes, please refer to the documentation.

  1. Run the app once more, and notice that the details pane is now locked.

Congratulations on adding the SlidingPaneLayout to your app!

10. Solution code

The solution code for this codelab is in the project and module shown below.

To get the code for this codelab and open it in Android Studio, do the following.

Get the code

  1. Click on the provided URL. This opens the GitHub page for the project in a browser.
  2. On the GitHub page for the project, click the Code button, which brings up a dialog.

5b0a76c50478a73f.png

  1. In the dialog, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
  2. Locate the file on your computer (likely in the Downloads folder).
  3. Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.

Open the project in Android Studio

  1. Start Android Studio.
  2. In the Welcome to Android Studio window, click Open an existing Android Studio project.

36cc44fcf0f89a1d.png

Note: If Android Studio is already open, instead, select the File > New > Import Project menu option.

21f3eec988dcfbe9.png

  1. In the Import Project dialog, navigate to where the unzipped project folder is located (likely in your Downloads folder).
  2. Double-click on that project folder.
  3. Wait for Android Studio to open the project.
  4. Click the Run button 11c34fc5e516fb1c.png to build and run the app. Make sure it builds as expected.
  5. Browse the project files in the Project tool window to see how the app is set-up.

11. Learn more