With the ability to run Android apps on Chromebooks, a huge ecosystem of apps and vast new functionality is now available to users. While this is great news for developers, certain app optimizations are required to meet usability expectations and to make for an excellent user experience. This code lab walks you through the most common of these optimizations.

What you will build

You will build a functional Android app that demonstrates best practices and optimizations for Chrome OS. Your app will:

Handle keyboard input including

Handle mouse input including

Use Architecture Components to

What you'll learn

What you'll need

Clone the repository from GitHub

git clone https://github.com/googlecodelabs/optimized-for-chromeos

...or download a zip file of the repository and extract it

Download Zip

Import the Project

Try the App

What do you think?

Even though this app is quite basic, and the parts which feel broken are easy to address, the user experience is awful. Let's fix it!

If you typed a few secret messages using the keyboard, you will have noticed that the Enter key does not do anything. This is frustrating to the user.

The sample code below and the Handling Keyboard Actions documentation should do the trick.

MainActivity.java (onCreate)

messageField.setOnKeyListener(new View.OnKeyListener() {
   public boolean onKey(View v, int keyCode, KeyEvent keyEvent) {
       if ((keyEvent.getAction() == KeyEvent.ACTION_UP) &&
               (keyCode == KeyEvent.KEYCODE_ENTER)) {
           sendButton.performClick();
           return true;
       }
       return false;
   }
});

Test it out! Being able to send messages using only the keyboard is a much nicer user experience.

Wouldn't it be nice to navigate this app using only the keyboard? As is, the experience is poor - when a user has a keyboard in front of them, it is frustrating when an application doesn't respond to it.

One of the easiest ways to make views navigable via the arrow and tab keys is to make them focusable.

Examine the layout files and look at the Button and ImageView tags. Notice that the focusable attribute is set to false. Change this to true in XML:

activity_main.xml

android:focusable="true"

Or programmatically:

MainActivity.java

myView.setFocusable(true);

Try it out. You should be able to use the arrow keys and enter key to select dinosaurs, but depending on your OS version, your screen, and the lighting, you may not be able to see which item is currently selected. To help with this, set the background resource for the images to R.attr.selectableItemBackground.

MainActivity.java (onCreate)

TypedValue highlightValue = new TypedValue();
getTheme().resolveAttribute(R.attr.selectableItemBackground, highlightValue, true);

dinoImage1.setBackgroundResource(highlightValue.resourceId);
dinoImage2.setBackgroundResource(highlightValue.resourceId);
dinoImage3.setBackgroundResource(highlightValue.resourceId);
dinoImage4.setBackgroundResource(highlightValue.resourceId);

Usually, Android does a pretty good job in deciding which View is above, below, to the left, or to the right of the View currently focused. How well does it work in this app? Make sure to test both the arrow keys and the tab key. Does the focus change to the view you expect?

Things are (intentionally) just a little bit off in this example. As a user, these small glitches in input feedback can feel really frustrating.

The right-most dinosaur and the send button are the main culprits for incorrect focus change. How do they work by default? How should they work? To manually adjust the arrow/tab key behavior, you can use the following. (Hint: only override the ones that aren't working correctly.)

Arrow keys

android:nextFocusLeft="@id/view_to_left"
android:nextFocusRight="@id/view_to_right"
android:nextFocusUp="@id/view_above"
android:nextFocusDown="@id/view_below"

Tab key

android:nextFocusForward="@id/next_view"

Or programmatically:

Arrow keys

myView.setNextFocusLeftId(R.id.view_to_left);
myView.setNextFocusRightId(R.id.view_to_right);
myView.setNextFocusTopId(R.id.view_above);
myView.setNextFocusBottomId(R.id.view_below);

Tab key

myView.setNextFocusForwardId(R.id.next_view);

You are now able to select dinosaurs but depending on your screen, the lighting conditions, the view, and your vision, it may be difficult to see the highlighting for selected items. For example, in the image below the default is grey on grey.

To provide more prominent visual feedback for your users. Add the following to res/values/styles.xml under AppTheme:

res/values/styles.xml

<item name="colorControlHighlight">@color/colorAccent</item>

Gotta love that pink, but the kind of highlighting in the picture above may be too aggressive for what you want and looks messy if all of your images are not exactly the same dimension. By using a state list drawable, you can create a border drawable that only appears when an item is selected.

res/drawable/box_border.xml

<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:state_focused="true">
       <shape android:padding="2dp">
           <solid android:color="#FFFFFF" />
           <stroke android:width="1dp" android:color="@color/colorAccent" />
           <padding android:left="2dp" android:top="2dp" android:right="2dp"
               android:bottom="2dp" />
       </shape>
   </item>
</selector>

Now replace the highlightValue/setBackgroundResource lines from the previous step with this new box_border background resource:

MainActivity.java (onCreate)

dinoImage1.setBackgroundResource(R.drawable.box_border);
dinoImage2.setBackgroundResource(R.drawable.box_border);
dinoImage3.setBackgroundResource(R.drawable.box_border);
dinoImage4.setBackgroundResource(R.drawable.box_border);

Keyboard users expect common Ctrl-based shortcuts to work. So now you'll add Undo (Ctrl-Z) and Redo (Ctrl-Shift-Z) shortcuts to the app.

First create a simple click-history stack. Imagine a user has done 5 actions and presses Ctrl-Z twice so that action 4 and 5 are on the redo stack and 1, 2, and 3 on the undo stack. If the user hits Ctrl-Z again, action 3 moves from the undo stack to the redo stack. If they then hit Ctrl-Shift-Z, action 3 moves from the redo stack to the undo stack.

At the top of your main class, define the different click actions and create the stacks using ArrayDeque.

MainActivity.java

private static final int UNDO_MESSAGE_SENT = 1;
private static final int UNDO_DINO_CLICKED = 2;
private ArrayDeque<Integer> undoStack = new ArrayDeque<Integer>();
private ArrayDeque<Integer> redoStack = new ArrayDeque<Integer>();

Whenever a message is sent or a dinosaur is clicked, add that action to the undo stack. When a new action is taken, clear the redo stack. Update your click listeners as follows:

MainActivity.java

//In message send onClick listener
undoStack.push(UNDO_MESSAGE_SENT);
redoStack.clear();

...

//In image onClick listener
undoStack.push(UNDO_DINO_CLICKED);
redoStack.clear();

Now actually map the shortcut keys. Support for Ctrl- commands and, on Android O and later, for Alt- and Shift- commands can be added using dispatchKeyShortcutEvent.

MainActivity.java

@Override
public boolean dispatchKeyShortcutEvent(KeyEvent event) {
    if (event.getKeyCode() == KeyEvent.KEYCODE_Z) {
        //Undo action
        return true;
    }
    return super.dispatchKeyShortcutEvent(event);
}

Let's be picky in this case. To insist that only Ctrl-Z triggers the callback and not Alt-Z or Shift-Z, use hasModifiers. The undo stack operations are filled in below.

MainActivity.java

@Override
public boolean dispatchKeyShortcutEvent(KeyEvent event) {
    //Ctrl-z == Undo
    if (event.getKeyCode() == KeyEvent.KEYCODE_Z
        && event.hasModifiers(KeyEvent.META_CTRL_ON)) {
            Integer lastAction = undoStack.poll();
            if (null != lastAction) {
                redoStack.push(lastAction);

                switch (lastAction) {
                    case UNDO_MESSAGE_SENT:
                        mMessagesSent--;
                        messageCounterText.setText(Integer.toString(mMessagesSent));
                        break;

                    case UNDO_DINO_CLICKED:
                        mDinosClicked--;
                        clickCounterText.setText(Integer.toString(mDinosClicked));
                        break;

                    default:
                        Log.d("OptimizedChromeOS", "Error on Ctrl-z: Unknown Action");
                        break;
                }

                return true;
            }
    }

    return super.dispatchKeyShortcutEvent(event);
}

Test it out. Are things working as expected? Now add Ctrl-Shift-Z by using OR with the modifier flags.

MainActivity.java

//Ctrl-Shift-z == Redo
if ((event.getKeyCode() == KeyEvent.KEYCODE_Z)
        && event.hasModifiers(KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)) {
    Integer prevAction = redoStack.poll();
    if (null != prevAction) {
        undoStack.push(prevAction);

        switch (prevAction) {
            case UNDO_MESSAGE_SENT:
                mMessagesSent++;
                messageCounterText.setText(Integer.toString(mMessagesSent));
                break;

            case UNDO_DINO_CLICKED:
                mDinosClicked++;
                clickCounterText.setText(Integer.toString(mDinosClicked));
                break;

            default:
                Log.d("OptimizedChromeOS", "Error on Ctrl-Shift-z: Unknown Action");
                break;
        }

        return true;
    }
}

For many interfaces, users assume that a right-click with a mouse and a double-tap on a trackpad will bring up a context menu. In this app, we want to provide this context menu so users can send these cool dinosaur pictures to a best friend.

Creating a context menu includes right-click functionality automatically. In many cases this is all you need. There are 3 parts to this setup:

  1. Let the UI know this view has a context menu

Use registerForContextMenu on each view you want a context menu for, in this case the 4 images.

MainActivity.java

registerForContextMenu(dinoImage1);
registerForContextMenu(dinoImage2);
registerForContextMenu(dinoImage3);
registerForContextMenu(dinoImage4);
  1. Define how the context menu looks

Design a menu in XML that contains all the context options you need. For this, just add "Share".

res/menu/context_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu_item_share_dino"
        android:icon="@android:drawable/ic_menu_share"
        android:title="@string/menu_share" />
</menu>

Then, in your main activity class, override onCreateContextMenu and pass in the XML file.

MainActivity.java

@Override
public void onCreateContextMenu(ContextMenu menu, View v,
                ContextMenu.ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.context_menu, menu);
}
  1. Define the actions to take when a specific item is chosen

Lastly, define the action to take by overriding onContextItemSelected. Here, just show a quick Snackbar letting the user know the image was shared successfully.

MainActivity.java

@Override
public boolean onContextItemSelected(MenuItem item) {
    if (R.id.menu_item_share_dino == item.getItemId()) {
        Snackbar.make(findViewById(android.R.id.content),
            getString(R.string.menu_shared_message), Snackbar.LENGTH_SHORT).show();
        return true;
    } else {
        return super.onContextItemSelected(item);
    }
}

Test it out! If you right-click on an image, the context menu should appear.

MainActivity.java

myView.setOnContextClickListener(new View.OnContextClickListener() {
   @Override
   public boolean onContextClick(View view) {
       //display right-click options
       return true;
   }
});

Adding tooltip text that appears upon hover is an easy way to help users understand how your UI works or to provide additional information.

So let's add tooltips for each of the photos with the dinosaur's name using the setTootltipText() method.

MainActivity.java

TooltipCompat.setTooltipText(dinoImage1, getString(R.string.name_dino_1));
TooltipCompat.setTooltipText(dinoImage2, getString(R.string.name_dino_2));
TooltipCompat.setTooltipText(dinoImage3, getString(R.string.name_dino_3));
TooltipCompat.setTooltipText(dinoImage4, getString(R.string.name_dino_4));

It can be useful to add a visual feedback effect to certain views when a pointing device is hovering over them.

To add this type of feedback, use the code below to make the Send button turn green when the mouse hovers over it.

MainActivity.java (onCreate)

sendButton.setOnHoverListener(new View.OnHoverListener() {
    @Override
    public boolean onHover(View v, MotionEvent event) {
        int action = event.getActionMasked();

        switch(action) {
            case ACTION_HOVER_ENTER:
                ColorStateList colorStateList = new ColorStateList(new int[][]{{}}, 
                        new int[]{Color.argb(127, 0, 255, 0)});
                sendButton.setBackgroundTintList(colorStateList);
                return true;

            case ACTION_HOVER_EXIT:
                sendButton.setBackgroundTintList(null);
                return true;
        }

        return false;
    }
});

Add one more hover effect: Change the background image associated with the draggable TextView, so that the user knows that that text is draggable.

MainActivity.java

dragText.setOnHoverListener(new View.OnHoverListener() {
    @Override
    public boolean onHover(View v, MotionEvent event) {
        int action = event.getActionMasked();

        switch(action) {
            case ACTION_HOVER_ENTER:
                dragText.setBackgroundResource(R.drawable.hand);
                return true;

            case ACTION_HOVER_EXIT:
                dragText.setBackgroundResource(0);
                return true;
        }

        return false;
    }
});

Test it out! You should see a big hand graphic appear when your mouse hovers on the "Drag Me!" text. Even this garish feedback makes the user experience more tactile.

For more information, see the View.OnHoverListener and MotionEvent documentation.

In a desktop environment, it is natural to drag and drop items into an app, particularly from Chrome OS's file manager. In this step, set up a drop target that can receive files or plain text items. In the next section of the codelab, we will implement a draggable item.

First, create an empty OnDragListener. Have a look at its structure before starting to code:

MainActivity.java

protected class DropTargetListener implements View.OnDragListener {
    private AppCompatActivity mActivity;

    public DropTargetListener(AppCompatActivity mActivity) {
        this.mActivity = mActivity;
    }

    @Override
    public boolean onDrag(View v, DragEvent event) {
       final int action = event.getAction();

       switch(action) {
           case DragEvent.ACTION_DRAG_STARTED:
               return true;

           case DragEvent.ACTION_DRAG_ENTERED:
               return true;

           case DragEvent.ACTION_DRAG_LOCATION:
               return true;

           case DragEvent.ACTION_DRAG_EXITED:
               return true;

           case DragEvent.ACTION_DROP:
               return true;

           case DragEvent.ACTION_DRAG_ENDED:
               return true;

           default:
               Log.d("OptimizedChromeOS","Unknown action type received by DropTargetListener.");
               return false;
         }
    }
}

The onDrag() method is called whenever any of several different drag events occur: starting a drag, hovering over a drop zone, or when an item is actually dropped. Here is a summary of the different drag events:

ACTION_DRAG_STARTED

This event is triggered whenever any drag is started. Indicate here whether a target can receive a particular item (return true) or not (return false) and visually let the user know. The drag event will contain a ClipDescription that contains information about the item being dragged.

To determine if this drag listener can receive an item, examine the MIME type of the item. In this example, indicate that the target is a valid one by tinting the background light green.

MainActivity.java

case DragEvent.ACTION_DRAG_STARTED:
    // Limit the types of items that can be received
    if (event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)
        || event.getClipDescription().hasMimeType("application/x-arc-uri-list")) {
        // Greenify background colour so user knows this is a target
        v.setBackgroundColor(Color.argb(55,0,255,0));
        return true;
    }

    //If the dragged item is of an unrecognized type, not a valid target
    return false;

ENTERED, EXITED, and ENDED

ENTERED and EXITED is where the visual/haptic feedback logic goes. In this example, deepen the green when the item is hovering over the target zone to indicate the user can drop it. In ENDED, reset the UI to its normal non-drag and drop state.

MainActivity.java

case DragEvent.ACTION_DRAG_ENTERED:
   // Increase green background colour when item is over top of target
   v.setBackgroundColor(Color.argb(150,0,255,0));
   return true;

case DragEvent.ACTION_DRAG_EXITED:
   // Less intense green background colour when item not over target
   v.setBackgroundColor(Color.argb(55,0,255,0));
   return true;

case DragEvent.ACTION_DRAG_ENDED:
   // Restore background colour to transparent
   v.setBackgroundColor(Color.argb(0,255,255,255));
   return true;

ACTION_DROP

This is the event that occurs when the item is actually dropped on the target. Here is where the processing gets done.

Note: Chrome OS files need to be accessed using ContentResolver.

In this demo, the target may receive a plain text object or a file. For plain text, show the text in the TextView. If it is a file, copy the first 200 characters and show that.

MainActivity.java

case DragEvent.ACTION_DROP:
    requestDragAndDropPermissions(event); //Allow items from other apps
    ClipData.Item item = event.getClipData().getItemAt(0);

    if (event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
        //If this is a text item, simply display it in a new TextView.
        TextView textTarget = (TextView) v;

        textTarget.setTextSize(TypedValue.COMPLEX_UNIT_SP,30);
        textTarget.setText(item.getText());

    } else if (event.getClipDescription().hasMimeType("application/x-arc-uri-list")) {
        //If a file, read the first 200 characters and output them in a new TextView.

        //Note the use of ContentResolver to resolve the ChromeOS content URI.
        Uri contentUri = item.getUri();
        ParcelFileDescriptor parcelFileDescriptor;
        try {
            parcelFileDescriptor = getContentResolver().openFileDescriptor(contentUri, "r");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            Log.e("OptimizedChromeOS", "Error receiving file: File not found.");
            return false;
        }

        FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();

        final int MAX_LENGTH = 5000;
        byte[] bytes = new byte[MAX_LENGTH];

        try {
            FileInputStream in = new FileInputStream(fileDescriptor);
            try {
                in.read(bytes,0, MAX_LENGTH);
            } finally {
                in.close();
            }
        } catch (Exception ex) {}
        String contents = new String(bytes);

        final int CHARS_TO_READ = 200;
        int content_length = (contents.length() > CHARS_TO_READ) ? CHARS_TO_READ : 0;

        TextView textTarget = (TextView) v;

        textTarget.setTextSize(TypedValue.COMPLEX_UNIT_SP,10);
        textTarget.setText(contents.substring(0,content_length));

    } else {
        return false;
    }
    return true;

OnDragListener

Now that the DropTargetListener is set up, attach it to the view you wish to receive the dropped items.

MainActivity.java

dropText.setOnDragListener(new DropTargetListener(this));

Test it out! Remember you will need to drag in files from the Chrome OS file manager.

Now set up a draggable item. A drag process is usually triggered by a long-press on a view. To indicate an item can be dragged, create a LongClickListener that provides the system with the data being transferred and indicates what type of data it is. This is also where you configure the appearance of the item while it is being dragged.

Set up a plain text drag item that pulls a string out of a TextView. Set the content MIME type to ClipDescription.MIMETYPE_TEXT_PLAIN.

For visual appearance during the drag, use the built-in DragShadowBuilder for a standard translucent drag look. Check out Starting a Drag in the documentation for a more complex example.

Remember to set the DRAG_FLAG_GLOBAL flag to signify that this item can be dragged into other apps.

MainActivity.java

protected class TextViewLongClickListener implements View.OnLongClickListener {
    @Override
    public boolean onLongClick(View v) {
        TextView thisTextView = (TextView) v;
        String dragContent = "Dragged Text: " + thisTextView.getText();

        //Set the drag content and type
        ClipData.Item item = new ClipData.Item(dragContent);
        ClipData dragData = new ClipData(dragContent,
                    new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN}, item);

        //Set the visual look of the dragged object
        //Can be extended and customized. We use the default here.
        View.DragShadowBuilder dragShadow = new View.DragShadowBuilder(v);

        // Starts the drag, note: global flag allows for cross-app drag
        v.startDragAndDrop(dragData, dragShadow, null, View.DRAG_FLAG_GLOBAL);

        return false;
    }
}

And now add the LongClickListener to the draggable TextView.

MainActivity.java (onCreate)

dragText.setOnLongClickListener(new TextViewLongClickListener());

Try it out! Are you able to drag in the text from the TextView?

Your app should be looking pretty good - keyboard support, mouse support, and dinosaurs! However, in a desktop environment, users will be frequently resizing the app, maximizing, un-maximizing, flipping to tablet mode, and changing orientation. What happens to your dropped items, the sent message counter, and the click counter?

The Activity Lifecycle is important to understand when creating Android apps. As apps get more complicated, managing lifecycle states can be difficult. Fortunately, Architecture Components makes it easier to handle lifecycle concerns in a robust way. In this codelab, we are going to focus on using ViewModel and LiveData to preserve the app state.

ViewModel helps maintain UI-related data across lifecycle changes. LiveData works as an observer to automatically update UI elements.

Consider the data we want to keep track of in this app:

Examine the code for the ViewModel class that sets this up. Essentially, it contains getter and setters using a singleton pattern. The LiveData constructs may seem a bit cumbersome at first but they are extremely powerful.

DinoViewModel.java

public class DinoViewModel extends ViewModel {
    private MutableLiveData<Integer> messagesSent;
    private MutableLiveData<Integer> dinosClicked;
    private MutableLiveData<String> dropText;

    private ArrayDeque<Integer> undoStack;
    private ArrayDeque<Integer> redoStack;

    public ArrayDeque<Integer> getUndoStack() {
        if (this.undoStack == null) {
            this.undoStack = new ArrayDeque<Integer>();
        }
        return undoStack;
    }

    public ArrayDeque<Integer> getRedoStack() {
        if (this.redoStack == null) {
            this.redoStack = new ArrayDeque<Integer>();
        }
        return redoStack;
    }

    public MutableLiveData<Integer> setDinosClicked(int newNumClicks) {
        if (this.dinosClicked == null) {
            this.dinosClicked = new MutableLiveData<Integer>();
        }
        dinosClicked.setValue(newNumClicks);
        return dinosClicked;
    }

    public MutableLiveData<Integer> getDinosClicked() {
        if (this.dinosClicked == null) {
            this.dinosClicked = new MutableLiveData<Integer>();
            this.dinosClicked.setValue(0);
        }
        return dinosClicked;
    }

    public MutableLiveData<Integer> setMessagesSent(int newMessagesSent) {
        if (this.messagesSent == null) {
            this.messagesSent = new MutableLiveData<Integer>();
        }
        messagesSent.setValue(newMessagesSent);
        return messagesSent;
    }

    public MutableLiveData<Integer> getMessagesSent() {
        if (this.messagesSent == null) {
            this.messagesSent = new MutableLiveData<Integer>();
            this.messagesSent.setValue(0);
        }
        return messagesSent;
    }

    public MutableLiveData<String> setDropText(String newDropText) {
        if (this.dropText == null) {
            this.dropText = new MutableLiveData<String>();
        }
        dropText.setValue(newDropText);
        return dropText;
    }

    public MutableLiveData<String> getDropText() {
        if (this.dropText == null) {
            this.dropText = new MutableLiveData<String>();
            this.dropText.setValue("Drop Things Here!");
        }
        return dropText;
    }
}

In your main activity, get the ViewModel using ViewModelProvider. This will do all the lifecycle magic. For example, the undo and redo stacks will automatically maintain their state during resizing, orientation, and layout changes.

MainActivity.java (onCreate)

//Get the persistent ViewModel
mDinoModel = ViewModelProviders.of(this).get(DinoViewModel.class);

//Restore our stacks
undoStack = mDinoModel.getUndoStack();
redoStack = mDinoModel.getRedoStack();

For the LiveData variables, create and attach Observer objects and tell the UI how to change when the variables change.

MainActivity.java (onCreate)

final Observer<Integer> messageObserver = new Observer<Integer>() {
    @Override
    public void onChanged(@Nullable final Integer newCount) {
        messageCounterText.setText(Integer.toString(newCount));
    }
};
mDinoModel.getMessagesSent().observe(this, messageObserver);

final Observer<Integer> dinoClickObserver = new Observer<Integer>() {
    @Override
    public void onChanged(@Nullable final Integer newCount) {
        clickCounterText.setText(Integer.toString(newCount));
    }
};
mDinoModel.getDinosClicked().observe(this, dinoClickObserver);

final Observer<String> dropTargetObserver = new Observer<String>() {
    @Override
    public void onChanged(@Nullable final String newString) {
        dropText.setText(newString);
    }
};
mDinoModel.getDropText().observe(this, dropTargetObserver);

Once these observers are in place, the code in all of the click callbacks can be simplified to just modify the ViewModel variable data.

The code below shows how you do not need to directly manipulate the TextView objects—all of the UI elements with LiveData observers update automatically.

MainActivity.java

sendButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        mDinoModel.setMessagesSent(mDinoModel.getMessagesSent().getValue() + 1);
        messageField.getText().clear();
        undoStack.push(UNDO_MESSAGE_SENT);
        redoStack.clear();
    }
});

...

//Image click listener
@Override
public void onClick(View v) {
    mDinoModel.setDinosClicked(mDinoModel.getDinosClicked().getValue() + 1);
    undoStack.push(UNDO_DINO_CLICKED);
    redoStack.clear();
}

...

switch (lastAction) {
    case UNDO_MESSAGE_SENT:
        mDinoModel.setMessagesSent(mDinoModel.getMessagesSent().getValue() - 1);
        break;

    case UNDO_DINO_CLICKED:
        mDinoModel.setDinosClicked(mDinoModel.getDinosClicked().getValue() - 1);
        break;

...

switch (prevAction) {
    case UNDO_MESSAGE_SENT:
        mDinoModel.setMessagesSent(mDinoModel.getMessagesSent().getValue() + 1);
        break;

    case UNDO_DINO_CLICKED:
        mDinoModel.setDinosClicked(mDinoModel.getDinosClicked().getValue() + 1);
        break;

Try it out! How does it resize now? Are you in love with Architecture Components?

Check out the Android Lifecycles Codelab for a more detailed exploration of Architecture Components. This blog post is a great resource for understanding how ViewModel and onSavedInstanceState work and interact.

You did it! Great work! You have gone a long way to familiarizing yourself with the most common issues developers face when optimizing Android apps for Chrome OS.

Sample Source Code

Clone the repository from GitHub

git clone https://github.com/googlecodelabs/optimized-for-chromeos

...or download the repository as a Zip file

Download Zip