Building XML-Based UIs for Tablet and Foldable Devices

In the ever-evolving landscape of Android development, supporting a wide range of devices—from phones to tablets and foldable devices—is essential. While Jetpack Compose offers a modern approach to building UIs, many legacy applications and simpler UI structures still rely on XML layouts. Adapting XML-based UIs for tablets and foldable devices ensures a consistent and optimized user experience across different form factors.

Why Support Tablets and Foldable Devices?

  • Increased Screen Real Estate: Tablets and foldable devices offer more screen space, allowing for richer and more engaging user interfaces.
  • Enhanced User Experience: Optimized layouts on larger screens can significantly improve usability and user satisfaction.
  • Market Reach: Targeting tablets and foldables expands your app’s potential user base.
  • Professional Appearance: Apps that adapt seamlessly to different screen sizes look more polished and professional.

Basic Strategies for Supporting Multiple Screen Sizes

To create UIs that work well on a variety of screen sizes, you can use several Android features within XML layouts:

  • Different Layout Files: Provide alternate layouts for different screen sizes.
  • Size Qualifiers: Use qualifiers like -sw600dp, -w820dp, or -large to specify screen-size-specific layouts.
  • Smallest Width Qualifier: -swdp specifies the smallest screen width in density-independent pixels (dp).
  • Available Width Qualifier: -wdp specifies the available screen width.
  • Orientation Qualifier: -port (portrait) and -land (landscape) orientations.
  • Fragments: Use Fragments to create modular and reusable UI components.
  • ConstraintLayout: Leverages relative positioning to adapt views to various screen dimensions.

Step-by-Step Guide to Optimizing XML Layouts for Tablets and Foldables

Step 1: Understand Screen Size Qualifiers

Android uses resource qualifiers to load appropriate layouts based on device characteristics. Key qualifiers include:

  • Smallest Width (swdp): Best for differentiating between phones, tablets, and large tablets.
  • Available Width (wdp) and Height (hdp): Use to adjust for specific aspect ratios or to handle split-screen scenarios.
  • Screen Aspect Ratio: Helps optimize for tall screens like foldables in half-folded state (-long for longer aspect ratio screens).

Step 2: Create Alternate Layouts

To start, identify the layouts that need adaptation and create alternate versions in different directories:


res/layout/main_activity.xml         // Default layout (phones)
res/layout-sw600dp/main_activity.xml // For tablets (min width 600dp)
res/layout-w820dp/main_activity.xml  // For large tablets (min width 820dp)

In each of these layouts, adjust the size, position, and visibility of elements to best utilize the available screen space. For example, you might show more information on larger screens or use a different arrangement of UI components.

Step 3: Use Fragments for UI Reusability

Fragments are ideal for creating modular UIs. Define Fragments for distinct sections of your layout and then reuse or combine them differently based on the device.

Example Fragment Layout (res/layout/fragment_item_detail.xml):


<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <TextView
        android:id="@+id/item_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        android:padding="16dp"/>
    <TextView
        android:id="@+id/item_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="16sp"
        android:padding="16dp"/>
</LinearLayout>

Example Fragment Class (ItemDetailFragment.java or ItemDetailFragment.kt):


import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment

class ItemDetailFragment : Fragment() {

    private var itemTitle: TextView? = null
    private var itemDescription: TextView? = null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_item_detail, container, false)
        itemTitle = view.findViewById(R.id.item_title)
        itemDescription = view.findViewById(R.id.item_description)
        return view
    }

    fun setItemDetails(title: String, description: String) {
        itemTitle?.text = title
        itemDescription?.text = description
    }
}

In your Activity layouts (main_activity.xml, main_activity-sw600dp.xml), include Fragments accordingly.

Step 4: Adaptive Layouts with ConstraintLayout

ConstraintLayout allows flexible positioning of views based on constraints relative to other views or the parent layout. This helps in creating UIs that can adapt to various screen sizes.

Example:


<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/titleTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Welcome to My App"
        android:textSize="24sp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:gravity="center_horizontal"
        android:layout_marginTop="32dp"/>

    <EditText
        android:id="@+id/userInputEditText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:hint="Enter your name"
        app:layout_constraintTop_toBottomOf="@id/titleTextView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_marginStart="32dp"
        android:layout_marginEnd="32dp"
        android:layout_marginTop="16dp"/>

    <Button
        android:id="@+id/submitButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Submit"
        app:layout_constraintTop_toBottomOf="@id/userInputEditText"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_marginTop="16dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

In this example, views are constrained to each other and the parent layout, ensuring they adjust dynamically with the screen size.

Step 5: Supporting Foldable Devices

Foldable devices introduce additional challenges such as handling screen folding and unfolding states. Here are specific strategies:

  • Screen Ratio Handling: Use -long qualifier or window size classes (from Jetpack WindowManager) to target specific screen ratios and fold states.
  • Multi-Pane Layouts: For larger screens (unfolded state), show details side-by-side, instead of a single screen on smaller displays.
  • WindowManager Jetpack:

    Using `WindowManager` library to be reactive and up-to-date on the app situation during device reconfiguration helps on displaying more natural layouts without black or displaced spaces for instance.


// In your Activity:
import androidx.window.java.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import kotlinx.coroutines.flow.collect

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)

        val windowInfoTracker = WindowInfoTracker.getOrCreate(this)
        val lifecycleScope = lifecycleScope // Ensures lifecycle awareness

        lifecycleScope.launch {
            windowInfoTracker.windowLayoutInfo(this@MainActivity)
                .collect { layoutInfo: WindowLayoutInfo ->
                    // Handle layoutInfo to adapt the UI based on device folding state
                    val isFolded = layoutInfo.displayFeatures.any { it is FoldingFeature }

                    if (isFolded) {
                        // Adjust UI for folded state
                    } else {
                        // Adjust UI for unfolded state
                    }
                }
        }
    }
}

Step 6: Using Dimension Resources for Consistent Sizing

Instead of hardcoding sizes in layouts, define dimensions in res/values/dimens.xml and use these references across layouts. For different screen sizes, provide different dimens.xml files.


<!-- res/values/dimens.xml -->
<resources>
    <dimen name="default_padding">16dp</dimen>
    <dimen name="title_size">20sp</dimen>
</resources>

<!-- res/values-sw600dp/dimens.xml -->
<resources>
    <dimen name="default_padding">24dp</dimen>
    <dimen name="title_size">28sp</dimen>
</resources>

In your layout file:


<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="@dimen/default_padding"
    android:textSize="@dimen/title_size" />

Step 7: Test Thoroughly

Testing on emulators and physical devices covering different screen sizes, resolutions, and device states (folded, unfolded) is crucial to ensure your UI adapts correctly. Use Android Studio’s emulator with pre-configured tablet and foldable device profiles for this purpose.

Code Sample: Multi-Pane Layout on Tablets

On tablets, implement a master-detail layout with a list of items on one side and details on the other.

res/layout/main_activity.xml (For phones):


<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_list_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

res/layout-sw600dp/main_activity.xml (For tablets):


<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <FrameLayout
        android:id="@+id/item_list_container"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"/>

    <FrameLayout
        android:id="@+id/item_detail_container"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="2"/>
</LinearLayout>

Corresponding Activity Code (MainActivity.java or MainActivity.kt):


import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)

        if (findViewById(R.id.item_detail_container) != null) {
            // Tablet Layout: Show detail fragment if available
            val detailFragment = ItemDetailFragment().apply {
                arguments = Bundle().apply {
                    putString("title", "Initial Title")
                    putString("description", "Initial Description")
                }
            }
            supportFragmentManager.beginTransaction()
                .replace(R.id.item_detail_container, detailFragment)
                .commit()
        } else {
            // Phone Layout: Show list fragment only
            supportFragmentManager.beginTransaction()
                .replace(R.id.item_list_container, ItemListFragment())
                .commit()
        }
    }
}

Advanced Techniques

  • MotionLayout: Use MotionLayout to animate UI changes between states for smooth transitions, especially on folding/unfolding events.
  • LiveData and ViewModel: Implement architectural components like LiveData and ViewModel to manage data changes and persist UI state across configuration changes, enhancing responsiveness on multi-window or foldable transitions.

Conclusion

Supporting tablet and foldable devices involves strategic planning, utilization of resource qualifiers, adaptive layouts with ConstraintLayout, modular design using Fragments, and awareness of device folding states. By adapting XML-based UIs, you can deliver an optimized and consistent user experience across a broader spectrum of Android devices, thus enhancing usability, appeal, and reach.