Chapter 16
In This Chapter
Zeroing in on why a tablet is a different digital beast
Modifying your existing app to run on tablets
Downloading a tablet emulator
Using responsive layouts
Building product flavors
Creating a tablet‐only activity
You need to master some tricks of the trade to make your apps work on tablets and on phones. In this chapter, you can get an overview of the differences betweeen phones and tablets, and then find out how to design the Tasks application to work on both types of devices.
Android tablets and Android phones have some obvious differences, and size immediately comes to mind, but that is only one of the differences:
Android tablet screens tend not to extend past the 7‐to‐10‐inch range, and the largest phones max out around 6 inches.
The line between tablet and phone can blur at the 5‐inch mark. Some “tweener” devices are marketed as phones, and others with nearly the same specs are marketed as tablets.
Tablet orientation varies depending on usage, whereas almost all Android phones have settled on portrait orientation for their screens.
Many Android tablets are designed for wide‐screen media viewing, so they favor landscape orientation. Others, such as the Nexus 7 and Kindle Fire, are designed primarily for use in portrait mode. That’s not to say that you can’t run an app in portrait mode on a landscape tablet (or vice versa), but be aware that many users may run your app in an orientation other than the one in which you completed most of your testing.
Tablets and phones also have some differences in hardware design and operation that affect app design. This list describes them from the tablet perspective:
In addition, don’t be surprised if you have to design your app (or tweak an existing one) to accommodate new tablet features.
To help accommodate the differences, you use a few techniques to upgrade the Tasks app so that it can work on both tablets and phones.
Go with the flow when you’re designing your layout to fit multiple screen sizes. A flowing, or responsive, layout skips a lot of hassle and frustration for both the designer and the user.
If you’re familiar with iOS development from a few years back, you know that back then you only had a few screen sizes to worry about: a couple of iPhone sizes, and a couple of iPad sizes. Each size required both low‐ and high‐resolution images, but that was easy enough to handle: Design for iPhone first, and then for iPad, and then plug in the low‐ and high‐resolution images in the respective versions and you were done.
Android has never been quite as simple to design for. Layouts in Android need to “flow” — that is, resize and rearrange themselves — so that they can accommodate minor (and sometimes major) differences in the width and height of users’ devices. Android developers have dozens or hundreds of different sizes of devices to worry about.
It’s similar to designing for websites — when you’re building a website, you can’t assume that all users will view it in browser windows that are exactly the same size (800 x 600 pixels). Users may view the site from bigger (or sometimes smaller) browser windows; your design must be flexible enough to give a good experience to the whole range of sizes. Designing for Android makes the same requirement. In fact, iOS is now moving in this same direction, and encouraging their developers to use flowing layouts as well.
So how do you perform this bit of magic? For openers, don’t try to use fixed dimensions (such as 10px or 120dp) in your layouts. Instead, favor relative dimensions, such as "wrap_content" and "match_parent" as much as possible. The idea is to achieve a responsive layout that can resize to fit the device.
The following code shows a layout that makes too many assumptions about the device it’s on:
<?xml version="1.0" encoding="utf-8”?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300px"
android:layout_height="match_parent"
android:lines="1"
android:text="Occurrences of the word 'Internet' in the Gettysburg
Address: 0 (unverified)"/>
The code has multiple problems:
Figure 16-1 shows the TextView from the above code. It abruptly cuts off text midsentence because of the fixed size. If the developer had used a responsive layout, the text wouldn’t have been cut off.
Fixing this particular example is easy by changing the width of the TextView and replacing android:lines="1" with android:maxLines="3":
<?xml version="1.0" encoding="utf-8”?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:maxLines="3"
android:text="Occurrences of the word 'Internet' in the Gettysburg Address:
0 (unverified)"/>
Android uses fragments to help you deal with the additional real estate on tablets. The basic idea is that a typical phone activity centers on one, two, or three distinct groups of reusable onscreen items. Put each of these groups into its own fragment and it becomes easy to reuse across multiple activities.
You’re going to do exactly this — you’ll add the TaskEditFragment and the TaskListFragment from your two phone activities into a single new activity that tablet users will enjoy.
Figure 16-2 shows how Tasks fragments lay out on a phone and on a tablet.
The fragment is a handy feature for the designer, but how do you slice and dice fragments to show the right experience for the right device? The tablet’s relatively vast screen real estate (compared to a phone) can show one or two more fragments on a single activity.
You can use one layout containing a single activity for your phone and another layout containing multiple fragments for your tablet. For example, here’s the TaskListActivity layout for the phone size in the Tasks app:
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:name="com.dummies.tasks.fragment.TaskListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
And Listing 16-1 shows how you might modify the code using two fragments to create a two‐column layout on a tablet:
Listing 16‐1: Two‐Column Table Layout Example
<?xml version="1.0" encoding="utf-8?>"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="match_parent">
<fragment
android:id="@+id/list_fragment"
android:name="com.dummies.tasks.fragment.TaskListFragment"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
/>
<fragment
android:id="@+id/edit_fragment"
android:name="com.dummies.tasks.fragment.TaskEditFragment"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
/>
</LinearLayout>
This is very similar to what you’ll do later in the chapter to put two fragments next to each other.
First things first — you need a tablet on which to test your application. If you already have a tablet, you’re well on your way; if you don’t, then you need an emulator to emulate an Android device on your computer. Google calls these Android Virtual Devices (AVDs). Follow these steps to get the Google Nexus 7 AVD:
See Chapter 3 for a reminder of how to configure an emulator.
As mentioned in the “Three strategies for adding tablet support” sidebar, you are going to create two product “flavors” for your app — one for phones and one for tablets. Each flavor will have its own APK file. For the most part, the flavors will share the same code, but they can have some slight differences in code and configuration.
Add the following to the android section of your Tasks build.gradle:
productFlavors {
phone {}
tablet {}
}
Both flavors will use all of the default settings, so there is no need to add any configuration to either of them. Later, you will set up each flavor to use a slightly different set of files, but for now they’re identical. When you do this, the shared code and resources will go into src/main (shared by all flavors), and the code for phones and tablets will go into src/phone and src/tablet, respectively, as specified by the name of the flavors you defined in the previous code.
Now choose Build⇒Rebuild Project and build your project again. If you then choose View⇒Tool Windows⇒Messages, you should see your project being built for both phones and tablets as in Figure 16-3.
There’s more to do, so go through the next sections before you try to install and run your two flavors.
There are going to be slight differences between your phone and tablet apps. Both require their own AndroidManifest.xml file. For the most part, the configuration will remain in your existing AndroidManifest.xml, but you’ll need to make some specific changes for each flavor that goes into a phone and tablet AndroidManifest.xml. The information in the phone and tablet AndroidManifest.xml will override the information in the main AndroidManifest.xml.
First, go to your AndroidManifest.xml file and remove the following code:
<activity
android:name="com.dummies.tasks.activity.TaskListActivity"
. . . >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<intent-filter>
</activity>
The TaskListActivity will be used only on your phone. You create a different activity for tablets. So for that reason, you just removed it from the shared AndroidManifest.xml.
The next step is to create the phone’s AndroidManifest.xml. But before creating it, first create the phone directory by going to your src directory and creating a new directory named phone. The directory phone must match the name of the flavor you created in your build.gradle in the previous section.
Next, create a new AndroidManifest.xml file in the src/phone directory with the following contents:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application>
<activity android:name="com.dummies.tasks.activity.TaskListActivity" >
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
You just moved the TaskListActivity from the shared AndroidManifest.xml to the phone‐specific AndroidManifest.xml.
Now build and run your app to make sure you haven’t broken anything. If everything worked properly, the app should run identically to how it did before you split the AndroidManifest.xml.
There is one more thing you need to do: Tell the Google Play Store that the phone version of your app should only be visible and installable on phones, not tablets. If you do not do this, the Google Play Store will show the phone app to people on tablets!
Open the src/phone/AndroidManifest.xml file and add the code in bold:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<compatible-screens>
<!-- all small size screens -->
<screen android:screenSize="small" android:screenDensity="ldpi" /> →6
<screen android:screenSize="small" android:screenDensity="mdpi" />
<screen android:screenSize="small" android:screenDensity="hdpi" />
<screen android:screenSize="small" android:screenDensity="xhdpi"/>
<screen android:screenSize="small" android:screenDensity="480" /> →10
<!-- all normal size screens --/>
<screen android:screenSize="normal" android:screenDensity="ldpi" /> →12
<screen android:screenSize="normal" android:screenDensity="mdpi" />
<screen android:screenSize="normal" android:screenDensity="hdpi" />
<screen android:screenSize="normal" android:screenDensity="xhdpi"/>
<screen android:screenSize="normal" android:screenDensity="480" /> →16
</compatible-screens>
<application>
<activity android:name="com.dummies.tasks.activity.TaskListActivity" >
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
The compatible‐screens element is not used by Android or your app directly. It is used only by the Google Play Store to determine which devices this app is compatible with.
→ 10 Declares that your app is compatible with all five resolutions of "small" screens (ldpi, mdpi, hdpi, xhdpi, and xxhdpi). You’ll notice that instead of saying xxhdpi on line 10, we say 480. This is because, at the time of this writing, the compatible‐screens element does not yet support xxhdpi directly, so we need to use the numerical value for xxhdpi, which is 480.
→ 12—16 Does the same, but for "normal" size devices.
These two sections declare that your app will run fine on small and normal devices, but will not run on large or extra‐large devices (generally tablets).
Now that you have a fully functioning phone app again, it’s time to start working on the tablet app.
Create a directory named tablet inside the src directory. Inside the tablet directory, create an AndroidManifest.xml for tablets that looks like the following:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application>
<!-- Declare our main activity, which will be different than the
main activity for phones.
-->
<activity android:name=
"com.dummies.tasks.tablet.activity.TaskListAndEditorActivity" > →10
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
Just like the phone manifest in the previous section, this manifest declares a single activity that will be the main launcher activity for the app — except the activity for tablets is different from the one for phones.
You should receive an error on line 10. That’s okay; you create the TaskListAndEditorActivity in the next section.
There is one more thing to do. You must tell the Google Play Store that this app is available only for tablets and not for phones. Add the following to your manifest:
<?xml version="1.0" encoding="utf-8"?>
manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<compatible-screens>
<!-- all large size screens -->
<screen android:screenSize="large" android:screenDensity="ldpi" />
<screen android:screenSize="large" android:screenDensity="mdpi" />
<screen android:screenSize="large" android:screenDensity="hdpi" />
<screen android:screenSize="large" android:screenDensity="xhdpi"/>
<screen android:screenSize="large" android:screenDensity="480" />
<!-- all xlarge size screens -->
<screen android:screenSize="xlarge" android:screenDensity="ldpi" />
<screen android:screenSize="xlarge" android:screenDensity="mdpi" />
<screen android:screenSize="xlarge" android:screenDensity="hdpi" />
<screen android:screenSize="xlarge" android:screenDensity="xhdpi"/>
<screen android:screenSize="xlarge" android:screenDensity="480" />
</compatible-screens>
<application>
<!-- Declare our main activity, which will be different than the
main activity for phones.
-->
<activity android:name=
"com.dummies.tasks.tablet.activity.TaskListAndEditorActivity" >
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
Notice the difference between the phone manifest and the tablet manifest. In the phone manifest, you said the flavor supported small and normal screens, but in the tablet manifest you say it supports large and xlarge screens.
You’ve created and built the phone app. You’ve declared the manifest file for the tablet app. The next step is to create the TaskListAndEditorActivity which only tablets will use.
The first step is to create the directories you need. Create the following directories inside the tablet directory:
Then create a new file inside the tablet/java/com/dummies/tasks/ tablet/activity directory named TaskListAndEditorActivity.java with the following code:
public class TaskListAndEditorActivity extends Activity
implements OnEditTask, OnEditFinished →2
{
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_task_list_and_editor);
setActionBar((Toolbar) findViewById(R.id.toolbar));
}
/**
* Called when the user asks to edit or insert a task.
*/
@Override
public void editTask(long id) { →16
}
/**
* Called when the user finishes editing a task.
*/
@Override
public void finishEditingTask() { →23
}
}
The onCreate method creates a new activity, sets its content to the activity_task_list_and_editor.xml layout (which you will create), and sets its action bar to the toolbar element.
In addition, you’ll see the following happen on these lines:
→ 2 You declare that this activity will implement the OnEditTask and OnEditFinished interfaces.
→ 16‐23 The methods from the interfaces on line 2 are defined on these lines. Currently they are empty, but you will recall from Chapters 9 and 10 that these methods are used to figure out what the fragment will do when the user asks to edit a task and finishes editing a task. You will complete these methods in a later section.
Next, add the layout file. Create a new directory inside the src/tablet directory called res. Inside res, create a directory named layout, and inside the layout directory create the layout file named activity_task_list_and_editor.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"→2
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:baselineAligned="false">
<Toolbar →8
style="?android:actionBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:title="@string/app_name"
android:id="@+id/toolbar"/>
<LinearLayout →15
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:baselineAligned="false">
<fragment →21
android:id="@+id/list_fragment"
android:name="com.dummies.tasks.fragment.TaskListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<FrameLayout →28
android:id="@+id/edit_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"/>
</LinearLayout>
</LinearLayout>
This layout consists of three main parts:
If you look back to Listing 16-1 at the beginning of the chapter, you’ll see that the layout is pretty similar, but instead of having two fragments in the layout, it has one fragment and one placeholder. The reason the second fragment needs to be a placeholder is because the edit fragment cannot be instantiated without knowing the ID of the task that needs to be edited. Because the ID isn’t known at the time the layout is created, you must inflate the fragment manually so that you can specify the ID. Look back at Chapter 10 and you’ll see you needed to do the same thing there for the TaskEditFragment.
Discussion of the previous code in more detail:
→ 2 A vertical LinearLayout that holds the two fragments for your tablet layout. Set baselineAligned to false as recommended by Android lint to get a tiny bump in performance. See Chapter 17 for more information about Android lint.
→ 8 The toolbar for the activity. It won’t be styled automatically, so set its style to the theme’s actionBarStyle. Also set its title to the app’s name.
→ 15 A second LinearLayout, this one arranged horizontally. This LinearLayout is used to hold the two side‐by‐side fragments.
→ 21 The list view fragment, which occupies the leftmost third of the screen. It is using one‐third of the screen because its layout_weight is set to 1, whereas the layout_weight of the other fragment on line 28 is set to 2, and 1 out of 3 is one‐third. Remember, when using layout_weight, you must set your layout_width to 0.
→ 28 The edit view fragment, which occupies the remaining two of three parts of the screen.
The tablet app isn’t quite done yet, but it’s time to run it and see what happens. How do you choose whether to build the phone app or the tablet app? Android Studio gives you a simple way to choose.
Choose View⇒Tool Windows⇒Build Variants. You should see a tool window, listing the build types for each of your apps. Click the build variant for Tasks and change it to say tabletDebug (see Figure 16-4):
Table 16‐1 The Four Build Configurations for the Tasks App
buildTypes / productFlavors |
Debug |
Release |
Phone |
phoneDebug |
phoneRelease |
Tablet |
tabletDebug |
tabletRelease |
Your build.gradle may not explicitly list a debug buildType, but it is always there by default.
You can add additional buildTypes or productFlavors if you want, but be careful! The number of build configurations can explode very quickly if you add a bunch of new productFlavors or buildTypes.
Now choose Build⇒Make Project and build the tablet app. Then choose Run⇒Run ‘Tasks’ to run your app, and choose the tablet emulator you created earlier in this chapter. You should see a list view but no editor view, as in Figure 16-5.
It looks great! But the problem is that there is no way add or edit tasks. You need to fill out the editTask() and finishEditingTask() methods.
Recall from Chapter 10 that, on phones, the editTask() method started a new TaskEditActivity as shown in Listing 16-2 (do not add this code to your tablet app!):
Listing 16‐2: The editTask() Method for Phones (Not Tablets)
@Override
public void editTask(long id) {
// When we are asked to edit a reminder, start the
// TaskEditActivity with the id of the task to edit.
startActivity(new Intent(this, TaskEditActivity.class)
.putExtra(TaskEditActivity.EXTRA_TASKID, id));
}
Instead of starting a new activity on tablets, it makes more sense to use some of the empty real estate on the right‐hand side of the page as a task editor. So for tablets, you’ll use a different version of editTask(), which opens the editor fragment inside the current activity.
Add the following code to TaskListAndEditorActivity.java:
@Override
public void editTask(long id) {
TaskEditFragment fragment = TaskEditFragment.newInstance(id); →3
FragmentTransaction ft = getFragmentManager() →5
.beginTransaction();
ft.replace(R.id.edit_container, fragment,
TaskEditFragment.DEFAULT_FRAGMENT_TAG);
ft.addToBackStack(null); →10
ft.commit(); →12
}
This code should look familiar. It’s very similar to the code you used in Chapter 10 to instantiate the TaskEditFragment there. As a recap:
→ 3 Creates the fragment for the given task id.
→ 5 Adds the fragment to the activity. If there’s one already there (for example, the user clicks on another task), then replace it. Tag the fragment with a name (DEFAULT_FRAGMENT_TAG in this case) so that you can find it again later.
→ 10 Adds this change to the backstack, so that when the user clicks the Back button you’ll pop this editor off the stack. If you don’t do this, the whole activity closes when the user clicks the Back button, which will be disruptive and unexpected.
→ 12 Make it so!
Now the only thing left to do is implement finishEditingTask(). All this method needs to do is remove the fragment you just created, so add the code in bold:
@Override
public void finishEditingTask() {
FragmentManager fm = getFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
// Find the edit fragment using the tag,
// and remove it from the activity.
Fragment fragment = fm.findFragmentByTag(
TaskEditFragment.DEFAULT_FRAGMENT_TAG);
transaction.remove(fragment);
transaction.commit();
}
Run the app again and you should be able to add and edit tasks.
Do you remember adding that super cool code in Chapter 10 to change the colors of your window based on the color of the image being displayed? Well, that was pretty cool back then, but it looks a bit goofy now. For phones, the colors take over the entire window, but here in the tablet they only cover the right half of the page.
Let’s disable the color change for tablets but keep it there for phones.
First, define a SHOULD_USE_PALETTE field in your build configuration. Open build.gradle and add the lines in bold:
productFlavors {
phone {
buildConfigField ‘boolean’, ‘SHOULD_USE_PALETTE’, ‘true’
}
tablet {
buildConfigField ‘boolean’, ‘SHOULD_USE_PALETTE’, ‘false’
}
}
This creates a new field named SHOULD_USE_PALETTE in the BuildConfig class. The field is set to true for the APK built for phones, and to false for the APK built for tablets.
The next step is to use the field in your code. Open TaskEditFragment.java and add the code in bold:
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor task) {
. . .
// Set the thumbnail image
Picasso.with(getActivity())
.load(TaskListAdapter.getImageUrlForTask(taskId))
.into(
imageView,
new Callback() {
@Override
public void onSuccess() {
Activity activity = getActivity();
if (activity == null)
return;
// Don’t do this for tablets, only phones,
// since it doesn’t really work with a split
// screen view.
if( !BuildConfig.SHOULD_USE_PALETTE )
return;
. . .
}
Now run your app again on the tablet; the Palette behavior should be disabled. Verify that you haven’t broken anything on phones by changing your build variant to phoneDebug and running the app again on your Nexus 5 phone emulator. The Palette behavior should still be present on your phone app.
Congratulations! You now have a fully implemented version of your Tasks app designed for tablets!
35.171.45.182