Page 1


Exploring Android

by Mark L. Murphy


Exploring Android by Mark L. Murphy Copyright © 2017-2018 CommonsWare, LLC. All Rights Reserved. Printed in the United States of America. Printing History: May 2018:

Version 0.3

The CommonsWare name and logo, “Busy Coder's Guide”, and related trade dress are trademarks of CommonsWare, LLC. All other trademarks referenced in this book are trademarks of their respective frms. The publisher and author(s) assume no responsibility for errors or omissions or for damages resulting from the use of the information contained herein.


Table of Contents Headings formatted in bold-italic have changed since the last version. • Preface ◦ How the Book Is Structured ................................................................. ix ◦ Prerequisites ........................................................................................... x ◦ About the Updates ................................................................................. x ◦ What’s New in Version 0.3? ......................................................... x ◦ Warescription ....................................................................................... xi ◦ Book Bug Bounty ................................................................................ xii ◦ Source Code and Its License ............................................................. xiii ◦ Creative Commons and the Four-to-Free (42F) Guarantee .... xiii • What We Are Building ◦ The Purpose ........................................................................................... 1 ◦ The Core UI ............................................................................................ 1 ◦ What We Are Missing ........................................................................... 5 • Installing the Tools ◦ Step #1: Checking Your Hardware ....................................................... 9 ◦ Step #2: Setting Up Java and 32-Bit Linux Support ........................... 10 ◦ Step #3: Install Android Studio ............................................................ 11 ◦ Step #4: Install the SDKs and Add-Ons ............................................. 12 • Creating a Starter Project ◦ Step #1: Importing the Project ............................................................ 21 ◦ Step #2: Getting Ready for the x86 Emulator .................................... 24 ◦ Step #3: Setting Up the AVD ............................................................... 25 ◦ Step #4: Setting Up the Device ............................................................ 31 ◦ Step #5: Running the Project .............................................................. 36 • Modifying the Manifest and Gradle Files ◦ Read Me! ............................................................................................... 39 ◦ Some Notes About Relative Paths ..................................................... 40 ◦ Step #1: Supporting Screens ............................................................... 40 ◦ Step #2: Blocking Backups .................................................................. 41 ◦ Step #3: Ignoring Lint .......................................................................... 42 ◦ Step #4: Supporting Java 8 .................................................................. 43 ◦ What We Changed .............................................................................. 47 • Changing Our Icon ◦ Read Me! .............................................................................................. 49 ◦ Step #1: Getting the Replacement Artwork ....................................... 49 i


◦ Step #2: Changing the Icon ................................................................. 50 ◦ Step #3: Running the Result ................................................................ 58 ◦ What We Changed .............................................................................. 59 Adding a Library ◦ Read Me! ............................................................................................... 61 ◦ Step #1: Removing Unnecessary Cruft ................................................ 61 ◦ Step #2: Upgrading Our SDK Versions ............................................... 62 ◦ Step #3: Adding Support for RecyclerView ............................... 63 ◦ What We Changed .............................................................................. 65 Constructing a Layout ◦ Read Me! ............................................................................................... 67 ◦ Step #1: Examining What We Have And What We Want ................ 68 ◦ Step #2: Adding a RecyclerView ........................................................ 69 ◦ Step #3: Adjusting the TextView ......................................................... 78 ◦ What We Changed ............................................................................. 84 Setting Up the Action Bar ◦ Read Me! .............................................................................................. 86 ◦ Step #1: Adding an Icon ...................................................................... 86 ◦ Step #2: Defining an Item ................................................................... 87 ◦ Step #3: Loading and Responding to Our Options .......................... 94 ◦ Step #5: Trying It Out .......................................................................... 97 ◦ Step #6: Dealing with Crashes .................................................. 98 ◦ What We Changed ............................................................................ 100 Customizing Our Theme ◦ Read Me! .............................................................................................. 101 ◦ Step #1: Defining Some Colors .......................................................... 102 ◦ Step #2: Configuring the Custom Theme ........................................ 105 ◦ Step #3: Trying It Out ........................................................................ 106 ◦ What We Changed ............................................................................ 108 Setting Up an Activity ◦ Read Me! ............................................................................................. 109 ◦ Step #1: Creating the Stub Activity Class and Manifest Entry ........ 109 ◦ Step #2: Adding a WebView ............................................................... 112 ◦ Step #3: Launching Our Activity ....................................................... 115 ◦ Step #4: Defining Some About Text .................................................. 116 ◦ Step #5: Populating the WebView ..................................................... 117 ◦ What We Changed ............................................................................. 118 Integrating Fragments ◦ Read Me! ............................................................................................. 120 ◦ Step #1: Adding the Requisite Library .............................................. 120 ◦ Step #2: Migrating to FragmentActivity ............................................ 121 ii


◦ Step #3: Creating a Fragment ............................................................. 121 ◦ Step #4: Displaying the Fragment .................................................... 126 ◦ Step #5: Renaming Our Layout Resource ......................................... 127 ◦ What We Changed ............................................................................ 128 Defining a Model ◦ Read Me! ............................................................................................. 129 ◦ Step #1: Adding a Stub POJO ............................................................ 130 ◦ Step #2: Linking to AutoValue .......................................................... 130 ◦ Step #3: Adding the Annotation ........................................................ 131 ◦ Step #4: Defining a Builder ................................................................ 132 ◦ Step #5: Adding Properties ................................................................ 134 ◦ Step #6: Populating Some Default Properties ......................... 135 ◦ What We Changed ............................................................................ 136 Setting Up a Repository ◦ Read Me! ............................................................................................. 138 ◦ Step #1: Adding the Class .................................................................. 138 ◦ Step #2: Offering a Singleton ................................................... 138 ◦ Step #3: Creating Some Fake Data ........................................... 139 ◦ Step #4: Publishing Our Data ........................................................... 140 ◦ What We Changed ............................................................................. 141 Testing Our Repository ◦ Read Me! .............................................................................................. 143 ◦ Step #1: Examine Our Existing Tests ................................................ 144 ◦ Step #2: Decide on Instrumentation Tests vs. Unit Tests ............... 146 ◦ Step #3: Rename the Test Case ......................................................... 146 ◦ Step #4: Test the Repository .................................................... 147 ◦ Step #5: Run the Test Case ................................................................ 148 ◦ Step #6: Fix the Repository ............................................................... 149 ◦ What We Changed ............................................................................. 151 Populating Our RecyclerView ◦ Read Me! .............................................................................................. 153 ◦ Step #1: Adding Data Binding Support ............................................ 154 ◦ Step #2: Defining a Row Layout ........................................................ 154 ◦ Step #3: Adding a Stub ViewHolder .................................................. 161 ◦ Step #4: Creating a Stub Adapter ..................................................... 162 ◦ Step #5: Retrieving Our Model Data ................................................ 164 ◦ Step #6: Adding the Data Binding .................................................... 165 ◦ Step #7: Completing the Adapter ..................................................... 167 ◦ Step #8: Wiring Up the RecyclerView .............................................. 170 ◦ Step #9: Seeing the Results ................................................................ 172 ◦ What We Changed ............................................................................. 173 iii


• Extending the Repository ◦ Step #1: Adding and Testing add() ..................................................... 175 ◦ Step #2: Adding and Testing replace() ............................................. 178 ◦ Step #3: Adding and Testing delete() ............................................... 180 ◦ What We Changed ............................................................................. 181 • Tracking the Completion Status ◦ Step #1: Injecting Our RosterRowHolder .......................................... 183 ◦ Step #2: Binding to the Checked Event ............................................ 184 ◦ Step #3: Passing the Event Up the Chain ......................................... 185 ◦ Step #4: Saving the Change ............................................................... 187 ◦ What We Changed ............................................................................ 188 • Displaying an Item ◦ Step #1: Creating the Fragment ......................................................... 189 ◦ Step #2: Instantiating the Fragment ................................................. 190 ◦ Step #3: Responding to List Clicks .................................................... 191 ◦ Step #4: Displaying the (Empty) Fragment ..................................... 196 ◦ Step #5: Creating an Empty Layout .................................................. 199 ◦ Step #6: Setting Up Data Binding ..................................................... 199 ◦ Step #7: Adding the Completed Icon .............................................. 200 ◦ Step #8: Displaying the Description ................................................ 208 ◦ Step #9: Showing the Created-On Date ........................................... 210 ◦ Step #10: Adding the Notes ............................................................... 214 ◦ Step #11: Populating the Layout ........................................................ 218 ◦ What We Changed ............................................................................ 220 • Editing an Item ◦ Step #1: Creating the Fragment ......................................................... 224 ◦ Step #2: Instantiating the Fragment ................................................. 224 ◦ Step #3: Setting Up a Menu Resource .............................................. 225 ◦ Step #4: Showing the Action Item .................................................... 229 ◦ Step #5: Displaying the (Empty) Fragment ..................................... 230 ◦ Step #6: Creating an Empty Layout .................................................. 232 ◦ Step #7: Setting Up Data Binding ..................................................... 232 ◦ Step #8: Adding the CheckBox ......................................................... 233 ◦ Step #9: Creating the Description Field .................................. 234 ◦ Step #10: Adding the Notes Field ...................................................... 238 ◦ Step #11: Populating the Layout .............................................. 244 ◦ What We Changed ........................................................................... 246 • Saving an Item ◦ Step #1: Adding the Action Bar Item ................................................ 247 ◦ Step #2: Replacing the Item ............................................................... 251 ◦ Step #3: Returning to the Display Fragment ........................... 252 iv


◦ What We Changed ............................................................................ 254 Adding and Deleting Items ◦ Step #1: Removing the Sample Data ................................................. 255 ◦ Step #2: Showing an Empty View ..................................................... 256 ◦ Step #3: Adding an Add Action Bar Item ......................................... 257 ◦ Step #4: Launching the EditFragment for Adds ............................. 260 ◦ Step #5: Adjusting Our Save Logic ................................................... 263 ◦ Step #6: Hiding the Empty View ...................................................... 265 ◦ Step #7: Adding a Delete Action Bar Item ....................................... 267 ◦ Step #8: Deleting the Item ...................................................... 270 ◦ Step #9: Fixing the Delete-on-Add Problem ................................... 274 ◦ Step #10: Fix Our Tests ...................................................................... 275 ◦ What We Changed ............................................................................ 275 Defining a View State ◦ Read Me! ............................................................................................. 279 ◦ Step #1: Creating a Stub AutoValue POJO ...................................... 280 ◦ Step #2: Creating Some Factories ..................................................... 281 ◦ What We Changed ............................................................................ 282 Stubbing a ViewModel ◦ Read Me! ............................................................................................. 284 ◦ Step #1: Adding the Dependency ...................................................... 284 ◦ Step #2: Creating a Stub ViewModel ................................................ 284 ◦ Step #3: Lazy-Creating the ViewModel ............................................ 285 ◦ What We Changed ........................................................................... 286 Publishing LiveData ◦ Read Me! ............................................................................................ 288 ◦ Step #1: Holding a MutableLiveData ............................................... 288 ◦ Step #2: Publishing a ViewState ............................................. 289 ◦ Step #3: Rendering a ViewState ....................................................... 289 ◦ Step #4: Observing the Stream ......................................................... 291 ◦ Step #5: Pondering What We Are Missing ...................................... 292 ◦ What We Changed ............................................................................ 292 Adding Actions ◦ Step #1: Creating the Base Action Class ........................................... 294 ◦ Step #2: Defining Specific Actions ................................................... 294 ◦ Step #3: Building Factory Methods ................................................. 296 ◦ What We Changed ............................................................................ 297 Creating a Controller ◦ Read Me! ............................................................................................. 300 ◦ Step #1: Adding a Stub Class ............................................................. 300 ◦ Step #2: Accessing the Repository .................................................... 301 v


◦ Step #3: Depending Upon RxJava ..................................................... 301 ◦ Step #4: Subscribing to Actions ........................................................ 302 ◦ Step #5: Handling Add, Edit, and Delete Actions ........................... 303 ◦ Step #6: Connecting to the Controller ............................................. 304 ◦ Step #7: Publishing List Actions ....................................................... 305 ◦ Step #8: Publishing Edit Actions ...................................................... 306 ◦ What We Changed ............................................................................ 306 Defining Results ◦ Step #1: Creating the Base Result Class ............................................ 308 ◦ Step #2: Defining Specific Results .................................................... 308 ◦ Step #3: Building Factory Methods .................................................. 309 ◦ What We Changed ............................................................................ 310 Completing the MVI Flow ◦ Step #1: Publishing Results ................................................................. 311 ◦ Step #2: Mutating the ViewState ....................................................... 312 ◦ Step #3: Reducing the Results ........................................................... 314 ◦ Step #4: Publishing the ViewStates .................................................. 314 ◦ Step #5: Handling Modify and Delete Results .................................. 317 ◦ Step #6: Handling the Initial Load ................................................... 319 ◦ Step #7: Showing Items ...................................................................... 321 ◦ Step #8: Wrapping Up the Rendering ...................................... 324 ◦ Step #9: Trying It Out ........................................................................ 326 ◦ What We Changed ............................................................................ 327 Testing the MVI Flow ◦ Step #1: Adding a ControllerTest Class ............................................. 329 ◦ Step #2: Setting Up a Controller ....................................................... 330 ◦ Step #3: Testing the Initial Load .............................................. 331 ◦ Step #4: Testing Adds ........................................................................ 332 ◦ Step #5: Testing Modifications .......................................................... 333 ◦ Step #6: Testing Deletions ................................................................ 333 ◦ What We Changed ............................................................................ 336 Getting a Room ◦ Read Me! ............................................................................................. 337 ◦ Step #1: Requesting More Dependencies ................................. 338 ◦ Step #2: Defining an Entity ............................................................... 339 ◦ Step #3: Crafting a DAO .................................................................... 340 ◦ Step #4: Adding a Database (And Some Type Converters) ............. 343 ◦ What We Changed ............................................................................ 347 Integrating Room Into the Repository ◦ Step #1: Getting a Database ............................................................... 349 ◦ Step #2: Fixing the CRUD ................................................................. 352 vi


◦ Step #3: Fixing the Tests .................................................................... 353 ◦ Step #4: Integrating StrictMode .............................................. 356 ◦ What We Changed ................................................................... 359 • Tracking Our Load Status ◦ Step #1: Add an isLoaded() Property ........................................ 361 ◦ Step #2: Updating the Loaded Status ...................................... 362 ◦ Step #3: Adjusting Our Layout ................................................. 363 ◦ Step #4: Reacting to the Loaded Status .................................. 366 ◦ What We Changed .................................................................. 368 • Filtering Our Items ◦ Step #1: Adding a Checkable Submenu ................................... 369 ◦ Step #2: Getting Control on Filter Choices .............................. 377 ◦ Step #3: Defining a Filter Action and Result ........................... 378 ◦ Step #4: Emitting and Controlling the Filter Action .............. 380 ◦ Step #5: Updating the ViewState .............................................. 382 ◦ Step #6: Filtering the Items .................................................... 384 ◦ Step #7: Using the Filtered Items ............................................. 385 ◦ Step #8: Fixing the Empty Text ............................................... 386 ◦ What We Changed .................................................................. 392

vii


Preface

Thanks! First, thanks for your interest in Android app development! Android is the world’s most popular operating system, but its value comes from apps written by developers like you. Also, thanks for your interest in this book! Hopefully, it can help “spin you up” on how to create Android applications that meet your needs and those of your users. And thanks for your interest in CommonsWare! The Warescription program makes this book and others available, to help developers like you craft the apps that your users need.

How the Book Is Structured Many books — such as The Busy Coder’s Guide to Android Development, — present programming topics, showing you how to use different APIs, tools, and so on. This book is different. This book has you build an app from the beginning. Whereas traditional programming guides are focused on breadth and depth, this book is focused on “hands-on”, guiding you through the steps to build the app. It provides a bit of details on the underlying concepts, but it relies on other resources — such as The Busy Coder’s Guide to Android Development — for the full explanation of those details. Instead, this book provides step-by-step instructions for building the app. If you are the sort of person who “learns by doing”, then this book is for you! ix


PREFACE

Prerequisites This book is targeted at developers starting out with Android app development. You will want another educational resource to go along with this book. The book will cross-reference The Busy Coder’s Guide to Android Development, but you can use other programming guides as well. This book shows you each step for building an app, but you will need to turn to other resources for answers to questions like “why do we need to do X?” or “what other options do we have than Y?”. Also, the app that you will create in this book works on Android 5.0+ devices and emulators. You will either need a suitable device or be in position to use the Android SDK emulator in order to build and run the app.

About the Updates This book will be updated a few times per year, to reflect new advances with Android, the libraries used by the sample app, and the development tools. If you obtained this book through the Warescription, you will be able to download updates as they become available, for the duration of your subscription period. If you obtained this book through other channels… um, well, it’s still a really nice book! Each release has notations to show what is new or changed compared with the immediately preceding release: • The Table of Contents shows sections with changes in bold-italic font • Those sections have changebars on the right to denote specific paragraphs that are new or modified And, there is the “What’s New” section, just below this paragraph.

What’s New in Version 0.3? Another 2 tutorials were added:

x


PREFACE • One to keep track of our loaded state, so we can properly show and hide a progress indicator while we are loading our data • One to add a “filter” option to the UI, so the user can limit the list to show either the outstanding or the completed items, in addition to all the items Also, Android Studio 3.1 was released in late March, so the tutorials are updated to work with that version of the IDE. In addition, a variety of bugs were fixed in the prose and code.

Warescription If you purchased the Warescription, read on! If you obtained this book from other channels, feel free to jump ahead. The Warescription entitles you, for the duration of your subscription, to digital editions of this book and its updates, in PDF, EPUB, and Kindle (MOBI/KF8) formats. You also have access to other titles that CommonsWare publishes during that subscription period, such as the aforementioned The Busy Coder’s Guide to Android Development. Each subscriber gets personalized editions of all editions of each title. That way, your books are never out of date for long, and you can take advantage of new material as it is made available. However, you can only download the books while you have an active Warescription. There is a grace period after your Warescription ends: you can still download the book until the next book update comes out after your Warescription ends. After that, you can no longer download the book. Hence, please download your updates as they come out. You can find out when new releases of this book are available via: 1. The CommonsBlog 2. The CommonsWare Twitter feed 3. The Warescription newsletter, which you can subscribe to off of your Warescription page 4. Just check back on the Warescription site every month or two Subscribers also have access to other benefits, including:

xi


PREFACE • “Office hours” — online chats to help you get answers to your Android application development questions. You will find a calendar for these on your Warescription page. • A Stack Overflow “bump” service, to get additional attention for a question that you have posted there that does not have an adequate answer. • A discussion board for asking arbitrary questions about Android app development

Book Bug Bounty Find a problem in the book? Let CommonsWare know! Be the first to report a unique concrete problem in the current digital edition, and CommonsWare will extend your Warescription by six months as a bounty for helping CommonsWare deliver a better product. By “concrete” problem, we mean things like: 1. Typographical errors 2. Sample applications that do not work as advertised, in the environment described in the book 3. Factual errors that cannot be open to interpretation By “unique”, we mean ones not yet reported. Be sure to check the book’s errata page, though, to see if your issue has already been reported. One coupon is given per email containing valid bug reports. We appreciate hearing about “softer” issues as well, such as: 1. Places where you think we are in error, but where we feel our interpretation is reasonable 2. Places where you think we could add sample applications, or expand upon the existing material 3. Samples that do not work due to “shifting sands” of the underlying environment (e.g., changed APIs with new releases of an SDK) However, those “softer” issues do not qualify for the formal bounty program. Questions about the bug bounty, or problems you wish to report for bounty consideration, should be sent to bounty@commonsware.com.

xii


PREFACE

Source Code and Its License The source code samples shown in this book are available for download from the book’s GitHub repository. All of the Android projects are licensed under the Apache 2.0 License, in case you have the desire to reuse any of it. Copying source code directly from the book, in the PDF editions, works best with Adobe Reader, though it may also work with other PDF viewers. Some PDF viewers, for reasons that remain unclear, foul up copying the source code to the clipboard when it is selected.

Creative Commons and the Four-to-Free (42F) Guarantee Each CommonsWare book edition will be available for use under the Creative Commons Attribution-Noncommercial-ShareAlike 3.0 license as of the fourth anniversary of its publication date, or when 4,000 copies of the edition have been sold, whichever comes first. That means that, once four years have elapsed (perhaps sooner!), you can use this prose for non-commercial purposes. That is our Four-toFree Guarantee to our readers and the broader community. For the purposes of this guarantee, new Warescriptions and renewals will be counted as sales of this edition, starting from the time the edition is published. This edition of this book will be available under the aforementioned Creative Commons license on 1 May 2022. Of course, watch the CommonsWare Web site, as this edition might be relicensed sooner based on sales. For more details on the Creative Commons Attribution-Noncommercial-ShareAlike 3.0 license, visit the Creative Commons Web site Note that future editions of this book will become free on later dates, each four years from the publication of that edition or based on sales of that specific edition. Releasing one edition under the Creative Commons license does not automatically release all editions under that license.

xiii


What We Are Building

By following the instructions in this book, you will build an Android app. But first, let’s see what the app is that you are building.

The Purpose Everybody has stuff to do. Ever since we have had “digital assistants” — such as the venerable Palm line of PDAs – a common use has been for tracking tasks to be done. So-called “to-do lists” are a popular sort of app, whether on the Web, on the desktop, or on mobile devices. The world has more than enough to-do list apps. Google themselves have published a long list of sample apps that use a to-do list as a way of exploring various GUI architectures. So, let’s build another one! Ours is not a fork of Google’s, but rather a “cleanroom” implementation of a to-do list with similar functionality.

The Core UI There are three main screens that the user will spend time in: the roster of to-do items, a screen with details of a particular item, and a screen for either adding a new item or editing an existing one. There is also an “about” screen for displaying information about the app.

1


WHAT WE ARE BUILDING

The Roster When initially launched, the app will show a roster of the recorded to-do items, if there are any. Hence, on the first run, it will show just an “empty view”, prompting the user to click the “add” action bar item to add a new item:

Figure 1: ToDo App, As Initially Launched, with No Items Once there are some items in the database, the roster will show those items, in alphabetical order by description, with a checkbox indicating whether or not they have been completed:

2


WHAT WE ARE BUILDING

Figure 2: ToDo App, Showing Some Items From here, the user can tap the checkbox to quickly mark an item as completed (or un-mark it if needed).

The Details A simple tap on an item in the roster brings up the details screen:

3


WHAT WE ARE BUILDING

Figure 3: ToDo App, Showing a Completed Item This just shows additional information about the item, including any notes the user entered to provide more detail than the simple description that gets shown in the roster. The checkmark icon will appear for completed items. From here, the user can edit this item (via the “pencil” icon).

The Editor The editor is a simple form, either to define a new to-do item or edit an existing one. If the user taps on the “add” action bar item from the roster, the editor will appear blank, and submitting the form will create a new to-do item. If the user taps on the “edit” (pencil) action bar item from the details screen, the editor will have the existing item’s data, which can be altered and saved:

4


WHAT WE ARE BUILDING

Figure 4: ToDo App, Editing a Completed Item Clicking the “save” toolbar button will either add the new item or edit the item that the user requested to edit. For an edit, the “delete” toolbar button will be available and will allow the user to delete this specific item, after confirmation.

What We Are Missing This is Version 0.2 of the book. Completing all of the tutorials in this book will get you the screens that are shown above. It will also save those items in a database, so they will persist from run-to-run of the app. However, there will still be a lot of missing functionality: • While the UI works well on phones, it is not optimized for larger-screen devices, such as tablets • You can only delete items one at a time; there is no multiple-selection option to delete them as a group • There are no import or export options, or any way to use this information other than from within the app itself

5


WHAT WE ARE BUILDING Those features and more will be added in new tutorials added to upcoming versions of the book.

6


Phase One: Getting a GUI


Installing the Tools

First, let us get you set up with the pieces and parts necessary to build an Android app. Specifically, in this tutorial, we will set up Android Studio.

Step #1: Checking Your Hardware Compiling and building an Android application, on its own, can be a hardwareintensive process, particularly for larger projects. Beyond that, your IDE and the Android emulator will stress your development machine further. Of the two, the emulator poses the bigger problem. The more RAM you have, the better. 8GB or higher is a very good idea if you intend to use an IDE and the emulator together. If you can get an SSD for your data storage, instead of a conventional hard drive, that too can dramatically improve the IDE performance. A faster CPU is also a good idea. The Android SDK emulator, as of 2016, supports CPUs with multiple cores — previously, it only supported a single core. However, other processes on your development machine will be competing with the emulator for CPU time, and so the faster your CPU is, the better off you will be. Ideally, your CPU has 2 to 4 cores, each 2.5GHz or faster at their base speed. There are two types of emulator: x86 and ARM. These are the two major types of CPUs used for Android devices. You really want to be able to use the x86 emulator, as the ARM emulator is extremely slow. However, to do that, you need a CPU with certain features:

9


INSTALLING THE TOOLS Development OS Windows Mac Linux

CPU Requirements an Intel CPU with support for VT-x, EM64T, and “Execute Disable” (XD) any an Intel CPU with support for VT-x, EM64T, and “Execute Disable” (XD), or an AMD CPU with support for AMD-V

Also, at least for newer API levels, your CPU must support SSSE3 extensions, though the details of this requirement are not documented as of October 2017. If your CPU does not meet those requirements, you will want to have 1+ Android devices available to you, so that you can test on hardware. Also, if you are running Windows or Linux, you need to ensure that your computer’s BIOS is set up to support Intel’s virtualization extensions. Unfortunately, many PC manufacturers disable this by default. The details of how to get into your BIOS settings will vary by PC, but usually it involves rebooting your computer and pressing some function key on the initial boot screen. In the BIOS settings, you are looking for references to “virtualization” or “VT-x”. Enable them if they are not already enabled. macOS machines come with virtualization extensions pre-enabled.

Step #2: Setting Up Java and 32-Bit Linux Support When you write Android applications, you typically write them in Java source code. That Java source code is then turned into the stuff that Android actually runs (Dalvik bytecode in an APK file). Android Studio, starting with version 2.2, ships with its own private copy of OpenJDK 8, and it will use that by default. You are welcome to install and use your own JDK if you wish, though ideally it is for Java 8. If your development OS is Linux, make sure that you can run 32-bit Linux binaries. This may or may not already be enabled in your Linux distro. For example, on Ubuntu 14.10, you may need to run the following to get the 32-bit binary support installed that is needed by the Android build tools: sudo apt-get install libncurses5:i386 libstdc++6:i386 zlib1g:i386

10


INSTALLING THE TOOLS

Step #3: Install Android Studio At the time of this writing, the current production version of Android Studio is 3.1, and this book covers that version. If you are reading this in the future, you may be on a newer version of Android Studio, and there may be some differences between what you have and what is presented here. You have two major download options. You can get the latest shipping version of Android Studio from the Android Studio download page.

Figure 5: Android Studio Download Page Or, you can download Android Studio 3.1 — the version used in this edition of this book — directly, for: • Windows • macOS • Linux Windows users can download a self-installing EXE, which will add suitable launch options for you to be able to start the IDE. Mac users can download a DMG disk image and install it akin to other Mac software, dragging the Android Studio icon into the Applications folder. Linux users (and power Windows users) can download a ZIP file, then unZIP it to some likely spot on your hard drive. Android Studio can then be run from the 11


INSTALLING THE TOOLS studio batch

file or shell script from your Android Studio installation’s bin/

directory.

Step #4: Install the SDKs and Add-Ons Next, we need to review what pieces of the Android SDK we have already and perhaps install some new items. To do that, you need to access the SDK Manager. When you first run Android Studio, you may be asked if you want to import settings from some other prior installation of Android Studio:

Figure 6: Android Studio First-Run Settings Migration Dialog For most users, particularly those using Android Studio for the first time, the “I do not have…” option is the correct choice to make. Then, after a short splash screen and a potentially long “Finding Available SDK Components” progress dialog, you will be taken to the Android Studio Setup Wizard:

12


INSTALLING THE TOOLS

Figure 7: Android Studio Setup Wizard, First Page Just click “Next” to advance to the second page of the wizard:

13


INSTALLING THE TOOLS

Figure 8: Android Studio Setup Wizard, Second Page Here, you have a choice between “Standard” and “Custom” setup modes. Most likely, right now, the “Standard” route will be fine for your environment. If you go the “Standard” route and click “Next”, you should be taken to a wizard page where you can choose your UI theme:

14


INSTALLING THE TOOLS

Figure 9: Android Studio Setup Wizard, UI Theme Page Choose whichever you like, then click Next, to go to a wizard page to verify what will be downloaded and installed:

15


INSTALLING THE TOOLS

Figure 10: Android Studio Setup Wizard, Verify Settings Page Clicking Next may take you to a wizard page explaining some information about the Android emulator:

16


INSTALLING THE TOOLS

Figure 11: Android Studio Setup Wizard, Emulator Info Page What is explained on this page may not make much sense to you. That is perfectly normal, and we will get into what this page is trying to say later in the book. Just click “Finish” to begin the setup process. This will include downloading a copy of the Android SDK and installing it into a directory adjacent to where Android Studio itself is installed. When that is done, after clicking “Finish”, Android Studio will busily start downloading stuff to your development machine. If you are running Linux, and your installation crashes with an “Unable to run mksdcard SDK tool” error, go back to Step #2 and set up 32-bit support on your Linux environment. Clicking “Finish” will then take you to the Android Studio Welcome dialog:

17


INSTALLING THE TOOLS

Figure 12: Android Studio Welcome Dialog Then, in the welcome dialog, click Configure, to bring up a configuration drop-down list:

Figure 13: Android Studio Welcome Dialog, Configure Drop-Down List There, tap on SDK Manager to bring up the SDK Manager.

18


INSTALLING THE TOOLS

Using SDK Manager and Updating Your Environment You should now have the SDK Manager open, as part of the overall default settings for Android Studio:

Figure 14: Android SDK Manager, “SDK Platforms” Tab The “SDK Platforms” tab lists the versions of Android that you can compile against. The latest version of Android (or possibly a preview edition) is usually installed when you set up Android Studio initially. However, for the tutorials, please also check “Android 8.1 (Oreo)” in the list if it is not already checked, and then click the “Apply” button to download and install those versions. You may need to accept a license confirmation dialog as part of this process — review the license, and if you accept it, click Next to begin the download:

19


INSTALLING THE TOOLS

Figure 15: Android SDK Manager, License Confirmation Dialog When that has completed, you can click “Finish” to close up the download dialog, and then you will be returned to the SDK Manager. Then, you can close up the SDK Manager by clicking the OK button.

20


Creating a Starter Project

Creating an Android application first involves creating an Android “project”. As with many other development environments, the project is where your source code and other assets (e.g., icons) reside. And, the project contains the instructions for your tools for how to convert that source code and other assets into an Android APK file for use with an emulator or device, where the APK is Android’s executable file format. Hence, in this tutorial, we kick off development of a sample Android application, to give you the opportunity to put some of what you are learning in this book in practice.

Step #1: Importing the Project First, we need an Android project to work in. Normally, you would use the new-project wizard to create a new project. However, the problem with the new-project wizard is that Google keeps changing what the new-project wizard generates. In most situations, that is not a huge problem. However, it becomes a problem for tutorials like this one, as if Google changes what is in the new project, the tutorial’s instructions become out of date. So, instead, we will import an existing project, so we can start from a stable base. Visit the releases page of this book’s GitHub repository. Then, scroll down to this book’s version and download the ToDo-Starter.zip file for this version of the book. UnZIP that project to some place on your development machine. It will unZIP into a ToDo/ directory.

21


CREATING A STARTER PROJECT Then, look at the contents of gradle/wrapper/gradle-wrapper.properties. It should look like this: #Tue Nov 14 13:31:09 EST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\: \://services.gradle.org/distributions/gradle-4.4-all.zip (from T02-Project/ToDo/gradle/wrapper/gradle-wrapper.properties)

In particular, make sure that the distributionUrl points to a services.gradle.org URL. Never import a project into Android Studio without checking the distributionUrl, as a malicious person could have distributionUrl point to malware that Android Studio would load and execute. Then, import the project. From the Android Studio welcome dialog, that is handled by the “Import project (Eclipse ADT, Gradle, etc.)” option. From an existing open Android Studio IDE window, you would use File > New > Import Project… from the main menu. Importing a project brings up a typical directory-picker dialog. Pick the ToDo/ directory and click OK to begin the import process. This may take a while, depending on the speed of your development machine. A “Tip of the Day” dialog may appear, which you can dismiss. At this point, the IDE window should be open on your starter project:

22


CREATING A STARTER PROJECT

Figure 16: Android Studio, Showing ToDo Project If you are using a newer version of Android Studio than then one profiled in this book, you may be prompted to “upgrade” the project. This would replace or modify some of the project’s build instructions. To keep your files synchronized with the book’s instructions, opt out of the upgrade. It is also possible, if you are using a newer version of Android Studio, that you will be prompted to download the API Level 26 version of the Android SDK. That would occur if you do not already have that downloaded from the SDK Manager. If asked, agree to install the API Level 26 SDK. The “Project” tool — docked by default on the left side, towards the top — brings up a way for you to view what is in the project. Android Studio has several ways of viewing the contents of Android projects. The default one, that you are presented with when creating or importing the project, is known as the “Android view”:

Figure 17: Android Studio “Android View”

23


CREATING A STARTER PROJECT While you are welcome to navigate your project using it, the tutorial chapters in this book, where they have screenshots of Android Studio, will show the project view:

Figure 18: Android Studio “Project View” To switch to this view — and therefore match what the tutorials will show you — click the arrows to the right of tabs (“Android”, “Project Files”) in the earlier screenshot, to bring up a drop-down of available views. macOS users may instead need to click on the “Android” tab to bring up the drop-down. In the drop-down, choose “Project” to switch to the project view.

Step #2: Getting Ready for the x86 Emulator Your first decision to make is whether or not you want to bother setting up an emulator image right now. If you have an Android device, you may prefer to start testing your app on it, and come back to set up the emulator at a later point. In that case, skip to Step #4. Otherwise, here is what you may need to do, based on the operating system on your development machine.

Windows If your CPU met the requirements, and you successfully enabled the right things in your system’s BIOS, the Android Studio installation should have installed HAXM, and you should be ready to go.

24


CREATING A STARTER PROJECT If, on the other hand, you got some error messages in the installation wizard regarding HAXM, you would need to address those first.

Mac The wizards of Cupertino set up their Mac hardware to be able to run the Android x86 emulator, which is awfully nice of them, considering that Android competes with iOS. The Android Studio installation wizard should have installed HAXM successfully, and you should be able to continue with the next step of the tutorial.

Linux The Android x86 emulator on Linux does not use HAXM. Instead, it uses KVM, a common Linux virtualization engine. If, during the Android Studio installation process, the wizard showed you a page that said that you needed to configure KVM, you will need to do just that before you can set up and use the x86 emulator. The details of how to set up KVM will vary by Linux distro (e.g., Ubuntu).

Step #3: Setting Up the AVD The Android emulator can emulate one or several Android devices. Each configuration you want is stored in an “Android virtual device”, or AVD. The AVD Manager is where you create these AVDs. Note that Android Studio now has its own implementation of the AVD Manager that is separate from the one Android developers have traditionally used. You may see screenshots of the older AVD Manager in blog posts, Stack Overflow answers, and the like. The AVD Manager still fills the same role, but it has a different look and feel. To open the AVD Manager in Android Studio, choose Tools > AVD Manager from the main menu. You should be taken to “welcome”-type screen:

25


CREATING A STARTER PROJECT

Figure 19: Android Studio AVD Manager, Welcome Screen Click the “Create Virtual Device” button, which brings up a “Virtual Device Configuration” wizard:

26


CREATING A STARTER PROJECT

Figure 20: Android Studio Virtual Device Configuration Wizard, First Page The first page of the wizard allows you to choose a device profile to use as a starting point for your AVD. The “New Hardware Profile” button allows you to define new profiles, if there is no existing profile that meets your needs. Since emulator speeds are tied somewhat to the resolution of their (virtual) screens, you generally aim for a device profile that is on the low end but is not completely ridiculous. For example, an 800x480 or 1280x768 phone would be considered by many people to be fairly low-resolution. However, there are plenty of devices out there at that resolution (or lower), and it makes for a reasonable starting emulator. If you want to create a new device profile based on an existing one — to change a few parameters but otherwise use what the original profile had – click the “Clone Device” button once you have selected your starter profile. However, in general, at the outset, using an existing profile is perfectly fine. The Galaxy Nexus or Nexus 4 images are likely choices to start with. Clicking “Next” allows you to choose an emulator image to use:

27


CREATING A STARTER PROJECT

Figure 21: Android Studio Virtual Device Configuration Wizard, Second Page The emulator images are spread across three tabs: • “Recommended” • “x86 Images” • “Other Images” For the purposes of the tutorials, you do not need an emulator image with the “Google APIs” — those are for emulators that have Google Play Services in them and related apps like Google Maps. However, in terms of API level, you can choose anything from API Level 21 (Android 5.0) on up. If you click on the x86 Images tab, you should see some images with a “Download” link, and possibly others without it:

28


CREATING A STARTER PROJECT

Figure 22: Android Studio Virtual Device Configuration Wizard, x86 Images The emulator images with “Download� next to them will trigger a one-time download of the files necessary to create AVDs for that particular API level and CPU architecture combination, after another license dialog and progress dialog:

Figure 23: Android Studio Component Installer Dialog, Downloading API 23 ARM Image

29


CREATING A STARTER PROJECT Once you have identified the image(s) that you want — and have downloaded any that you did not already have — click on one of them in the wizard:

Figure 24: Android Studio Virtual Device Configuration Wizard, After Choosing Image Clicking “Next” allows you to finalize the configuration of your AVD:

30


CREATING A STARTER PROJECT

Figure 25: Android Studio Virtual Device Configuration Wizard, Third Page A default name for the AVD is suggested, though you are welcome to replace this with your own value. Change the AVD name, if necessary, to something valid: only letters, numbers, spaces, and select punctuation (e.g., ., _, -, (, )) are supported. The rest of the default values should be fine for now. Clicking “Finish” will return you to the main AVD Manager, showing your new AVD. You can then close the AVD Manager window.

Step #4: Setting Up the Device You do not need an Android device to get started in Android application development. Having one is a good idea before you try to ship an application (e.g., upload it to the Play Store). And, perhaps you already have a device – maybe that is what is spurring your interest in developing for Android.

31


CREATING A STARTER PROJECT If you do not have an Android device that you wish to set up for development, skip this step. The first thing to do to make your device ready for use with development is to go into the Settings application on the device. On Android 8.0+, go into System > About phone. On older devices, About is usually a top-level entry. In the About screen, tap on the build number seven times, then press BACK, and go into “Developer options” (which was formerly hidden)

Figure 26: Developer Options, in Settings App You may need to slide a switch in the upper-right corner of the screen to the “ON” position to modify the values on this screen. Generally, you will want to scroll down and enable USB debugging, so you can use your device with the Android build tools:

32


CREATING A STARTER PROJECT

Figure 27: Debugging Options, in Settings App You can leave the other settings alone for now if you wish, though you may find the “Stay awake� option to be handy, as it saves you from having to unlock your phone all of the time while it is plugged into USB. Note that on Android 4.2.2 and higher devices, before you can actually use the setting you just toggled, you will be prompted to allow USB debugging with your specific development machine via a dialog box:

33


CREATING A STARTER PROJECT

Figure 28: Allow USB Debugging Dialog This occurs when you plug in the device via the USB cable and have the driver appropriately set up. That process varies by the operating system of your development machine, as is covered in the following sections.

Windows When you first plug in your Android device, Windows will attempt to find a driver for it. It is possible that, by virtue of other software you have installed, that the driver is ready for use. If it finds a driver, you are probably ready to go. If the driver is not found, here are some options for getting one. Windows Update Some versions of Windows (e.g., Vista) will prompt you to search Windows Update for drivers. This is certainly worth a shot, though not every device will have supplied its driver to Microsoft. Standard Android Driver In your Android SDK installation, if you chose to install the “Google USB Driver� package from the SDK Manager, you will find an extras/google/usb_driver/ 34


CREATING A STARTER PROJECT directory, containing a generic Windows driver for Android devices. You can try pointing the driver wizard at this directory to see if it thinks this driver is suitable for your device. This will often work for Nexus devices. Manufacturer-Supplied Driver If you still do not have a driver, the OEM USB Drivers in the developer documentation may help you find one for download from your device manufacturer. Note that you may need the model number for your device, instead of the model name used for marketing purposes (e.g., GT-P3113 instead of “Samsung Galaxy Tab 2 7.0”).

macOS and Linux Odds are decent that simply plugging in your device will “just work”. You can see if Android recognizes your device via running adb devices in a shell (e.g., macOS Terminal), where adb is in your platform-tools/ directory of your SDK. If you get output similar to the following, the build tools detected your device: List of devices attached HT9CPP809576 device

If you are running Ubuntu (or perhaps other Linux variants), and this command did not work, you may need to add some udev rules. For example, here is a 51-android.rules file that will handle the devices from a handful of manufacturers: SUBSYSTEM=="usb", SYSFS{idVendor}=="0bb4", MODE="0666" SUBSYSTEM=="usb", SYSFS{idVendor}=="22b8", MODE="0666" SUBSYSTEM=="usb", SYSFS{idVendor}=="18d1", MODE="0666" SUBSYSTEMS=="usb", ATTRS{idVendor}=="18d1", ATTRS{idProduct}=="0c01", MODE="0666", OWNER="[me]" SUBSYSTEM=="usb", SYSFS{idVendor}=="19d2", SYSFS{idProduct}=="1354", MODE="0666" SUBSYSTEM=="usb", SYSFS{idVendor}=="04e8", SYSFS{idProduct}=="681c", MODE="0666"

Drop that in your /etc/udev/rules.d directory on Ubuntu, then either reboot the computer or otherwise reload the udev rules (e.g., sudo service udev reload). Then, unplug and re-plug in the device and see if it is detected.

35


CREATING A STARTER PROJECT

Step #5: Running the Project Now, we can confirm that our project is set up properly by running it on a device or emulator. To do that in Android Studio, just press the Run toolbar button (usually depicted as a green rightward-pointing triangle):

Figure 29: Android Studio Toolbar, Showing Run Button You will then be presented with a dialog indicating where you want the app to run: on some existing device or emulator, or on some newly-launched emulator:

Figure 30: Android Studio Device Chooser Dialog If you do not have an emulator running, choose one from the list, then click OK. Android Studio will launch your emulator for you. And, whether you start a new emulator instance or reuse an existing one, your app should appear on it:

36


CREATING A STARTER PROJECT

Figure 31: Android 8.1 Device, Showing ToDo App Note that you may have to unlock your device or emulator to actually see the app running. The first time you launch the emulator for a particular AVD, you may see this message:

Figure 32: Android Emulator Cold-Boot Warning The emulator now behaves a bit more like an Android device. Closing the emulator window used to be like completely powering off a phone, but now it is more like tapping the POWER button to turn off the screen. The next time you start that particular AVD, it will wake up to the state in which you left it, rather than booting from scratch (“cold boot”). This speeds up starting the emulator. Occasionally, though, you will have the need to start the emulator as if the device were powering on. To do that, in the AVD Manager, in the drop-down menu in the Actions column, choose “Cold Boot Now”.

37


Modifying the Manifest and Gradle Files

Now that we have our starter project, we need to start making changes, as we have a lot of work to do. In this tutorial, we will start with some “root” files, ones that form the backbone of our app: the manifest and the Gradle build scripts. Here, we will make a few changes, just to help get you familiar with editing these files. We will be returning to these files many times over the course of the rest of the book. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Read Me! This tutorial assumes that you have learned the basics of working with Gradle and the manifest file elsewhere. That could be: • From the official documentation, such as their introductory material on the manifest and Gradle-based build configuration • The Busy Coder’s Guide to Android Development and its “Introducing Gradle and the Manifest” chapter • Other educational resources

39


MODIFYING THE MANIFEST AND GRADLE FILES

Some Notes About Relative Paths In these tutorials, you will see references to relative paths, like AndroidManifest.xml, res/layout/, and so on. You should interpret these paths as being relative to the app/src/main/ directory within the project, except as otherwise noted. So, for example, Step #1 below will ask you to open AndroidManifest.xml — that file can be found in app/src/main/ AndroidManifest.xml from the project root.

Step #1: Supporting Screens Android devices come in a wide range of shapes and sizes. Our app can support them all. However, we should advise Android that we are indeed willing to support any screen size. To do this, we need to add a <supports-screens> element to the manifest. To do this, double-click on AndroidManifest.xml in the project explorer:

Figure 33: Android Studio, Showing Manifest Editor

40


MODIFYING THE MANIFEST AND GRADLE FILES As a child of the root <manifest> element, add a <supports-screens> element as follows: <supports-screens android:largeScreens="true" android:normalScreens="true" android:smallScreens="true" android:xlargeScreens="true"/> />

At this point, the manifest should resemble: <?xml version="1.0" encoding="utf-8"?> <manifest package="com.commonsware.todo" xmlns:android="http://schemas.android.com/apk/res/android"> > <supports-screens android:largeScreens="true" android:normalScreens="true" android:smallScreens="true" android:xlargeScreens="true"/> /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> > <activity android:name=".MainActivity"> > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>

Step #2: Blocking Backups If you look at the <application> element, you will see that it has a few attributes, including android:allowBackup="true". This attribute indicates that ToDo should participate in Androidâ&#x20AC;&#x2122;s automatic backup system.

41


MODIFYING THE MANIFEST AND GRADLE FILES That is not a good idea, until you understand the technical and legal ramifications of that choice. In the short term, change android:allowBackup to be false.

Step #3: Ignoring Lint Even after that change, the application element name may have a beige background. If you hover your mouse over it and look at the explanatory tooltip, you will see that it is complaining that this app is not indexable, and that you should add an ACTION_VIEW activity to the app. This is ridiculous. This app (hopefully) will never wind up on the Play Store, and so Google’s “app indexing” capability will never be relevant. Put your text cursor somewhere inside the application element name and press Alt-Enter (or Option-Return on macOS). This should bring up a popup window showing some “quick fixes” for the problem:

Figure 34: Android Studio “Quick Fixes” Tooltip Choose the “suppress” option. Then, press Ctrl-Alt-L (or Command-Option-L on macOS) to reformat the file. You will wind up with something like: <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:tools="http://schemas.android.com/tools" package="com.commonsware.todo" xmlns:android="http://schemas.android.com/apk/res/android"> > <supports-screens android:largeScreens="true" android:normalScreens="true" android:smallScreens="true" android:xlargeScreens="true" /> <application android:allowBackup="false" android:icon="@mipmap/ic_launcher"

42


MODIFYING THE MANIFEST AND GRADLE FILES android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> > <activity android:name=".MainActivity"> > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> (from T03-Manifest/ToDo/app/src/main/AndroidManifest.xml)

The <application> element now has a tools:ignore="GoogleAppIndexingWarning" attribute, and the root <manifest> element defines the tools XML namespace. The net effect is that we are telling the build tools â&#x20AC;&#x201D; specifically the Lint utility â&#x20AC;&#x201C; that it should ignore this particular issue.

Step #4: Supporting Java 8 In 2017, Android developers finally got decent official support for some Java 8 features, such as lambda expressions. However, you have to ask for them. By default, the build tools assume that you are writing code that does not use Java 8 features. If you want Java 8 capabilities, you need to modify your app/build.gradle file to ask for those capabilities. First, take a look at the current app/build.gradle file off of the project root directory: apply plugin: 'com.android.application' android { compileSdkVersion 26 defaultConfig { applicationId "com.commonsware.todo" minSdkVersion 21

43


MODIFYING THE MANIFEST AND GRADLE FILES targetSdkVersion 26 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support.constraint:constraint-layout:1.0.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' }

Since our Gradle build script fairly simple right now, it is safe to use the Project Structure dialog to make changes to them, as this is sometimes a bit easier than remembering how to make the changes directly. So, from the Android Studio main menu, choose “File” > “Project Structure”, to bring up the “Project Structure” dialog. There, click on the “app” entry in the list on the left, to bring up configuration options for your app module:

44


MODIFYING THE MANIFEST AND GRADLE FILES

Figure 35: Android Studio Project Structure Dialog On the first tab — labeled “Properties” — you will see a “Source Compatibility” and a “Target Compatibility” drop-down. Change each to “1.8”:

45


MODIFYING THE MANIFEST AND GRADLE FILES

Figure 36: Android Studio Project Structure Dialog, Showing Java 8 Compatibility Then, click OK to commit those changes and close this dialog. If you look back in the editor tab for app/build.gradle, you will see that we now have a compileOptions closure with our Java 8 request: apply plugin: 'com.android.application' android { compileSdkVersion 26 defaultConfig { applicationId "com.commonsware.todo" minSdkVersion 21 targetSdkVersion 26 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } }

46


MODIFYING THE MANIFEST AND GRADLE FILES dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'com.android.support.constraint:constraint-layout:1.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }

(from T03-Manifest/ToDo/app/build.gradle)

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • app/build.gradle • app/src/main/AndroidManifest.xml

47


Changing Our Icon

Our ToDo project has some initial resources, such as our app’s display name and its launcher icon. However, the defaults are not what we want for the long term. So, in addition to adding new resources in future tutorials, we will change the launcher icon in this tutorial. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Read Me! This tutorial assumes that you have learned the basics of working with drawable resources elsewhere. Ideally, that would be from the official documentation, but the documentation in this area is quite disorganized. Another possibility would be learning it from The Busy Coder’s Guide to Android Development and its “Some Words About Resources” chapter and its “Icons” chapter.

Step #1: Getting the Replacement Artwork First, we need something that visually represents a to-do list, particularly when shown as the size of an icon in a home screen launcher. This public domain piece of clipart will serve this purpose:

49


CHANGING OUR ICON

Figure 37: Checklist Clipart from OpenClipArt.org That clipart is available as an SVG, a PNG (in one of three resolutions), or a PDF. For the purposes of creating a launcher icon, the PNG is the simplest file format to use. Download the medium-resolution PNG file to some location on your development machine outside of the project directory.

Step #2: Changing the Icon Android Studio includes an Image Asset Wizard that is adept at creating launcher icons. This is important, as while creating launcher icons used to be fairly simple, Android 8.0 made them a lot more complicatedâ&#x20AC;Ś but the Image Asset Wizard hides most of that complexity. First, right-click over the res/ directory in your main source set in the project explorer:

50


CHANGING OUR ICON

Figure 38: Android Studio Project Tree Context Menu In that context menu, choose New > Image Asset from the context menu. That will bring up the Asset Studio wizard:

Figure 39: Android Studio Image Asset Wizard, As Initially Launched 51


CHANGING OUR ICON In the “Icon Type” drop-down, make sure that “Launcher Icons (Adaptive and Legacy)” is chosen — this should be the default. Also, ensure that the “Name” field has ic_launcher, which also should be the default. In the “Foreground Layer” tab, ensure that the “Layer Name” is ic_launcher_foreground. In the “Source Asset” group, ensure that the “Asset Type” is set to “Image”. Then, click the “…” button next to the “Path” field, and find the clipart that you downloaded in Step #1 above. When you load the image, it will be just a bit too big:

Figure 40: Android Studio Image Asset Wizard, With Custom Image To fix this, in the “Scaling” group, select “Yes” for “Trim”. Then, adjust the “Resize” slider until the clipart is inside the circular “safe zone” region in the previews. A “Resize” value of around 80% should work:

52


CHANGING OUR ICON

Figure 41: Android Studio Image Asset Wizard, With Scaled Custom Image Switch to the “Background Layer” tab and ensure that the “Layer Name” is ic_launcher_background. Then, switch the “Asset Type” to “Color”:

53


CHANGING OUR ICON

Figure 42: Android Studio Image Asset Wizard, Background Layer Tab, Using Default Color Background If you do not like the default color, tap the hex color value to bring up a color picker:

54


CHANGING OUR ICON

Figure 43: Android Studio Image Asset Wizard Color Picker Dialog Pick some other color, then click “Choose” to apply that to the icon background:

55


CHANGING OUR ICON

Figure 44: Android Studio Image Asset Wizard, Background Layer Tab, Using Custom Color Background Then, switch to the “Legacy” tab. Ensure that the “Generate” value is “Yes” for both “Legacy Icon” and “Round Icon”, but set it to “No” for “Google Play Store Icon” (as this app will not be published on the Play Store). Also, switch the “Shape” value for the “Legacy Icon” to “Circle”:

56


CHANGING OUR ICON

Figure 45: Android Studio Image Asset Wizard, Legacy Tab, Using Custom Settings That way, our icon should be the same on most pre-Android 8.0 devices. On Android 8.0+ devices — and on a few third-party home screens on older devices – our icon will be our clipart on our chosen background color, but with a shape determined by the home screen implementation. Click the “Next” button at the bottom of the wizard to advance to a confirmation screen:

57


CHANGING OUR ICON

Figure 46: Android Studio Image Asset Wizard, Confirmation Page There will be a warning that existing files will be overwritten. Since that is what we are intending to do, this is fine. Click â&#x20AC;&#x153;Finishâ&#x20AC;?, and Android Studio will generate your launcher icon.

Step #3: Running the Result If you run the resulting app, then go back to the home screen launcher, you will see that it shows up with the new icon:

58


CHANGING OUR ICON

Figure 47: Android Home Screen Launcher, Showing App Icon (Lower Right)

What We Changed The bookâ&#x20AC;&#x2122;s GitHub repository contains the entire result of having completed this tutorial. A number of files were changed in app/src/main/res/, as creating launcher icons is annoyingly complicated.

59


Adding a Library

Most of an Android app comes from code that you did not write. It comes from code written by others, in the form of libraries. Even though we have not gotten very far with the ToDo app, we are already using some libraries, and in this chapter, we will update that roster. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Read Me! This tutorial assumes that you have learned the basics of working with Gradle dependencies elsewhere. That could be: • From the official documentation, such as their material on adding build dependencies • The Busy Coder’s Guide to Android Development and its “Dependencies” chapter • Other educational resources

Step #1: Removing Unnecessary Cruft Open app/build.gradle in Android Studio. You will find that it contains a dependencies closure that looks like this: dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'com.android.support.constraint:constraint-layout:1.0.2'

61


ADDING A LIBRARY testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' }

This was code-generated for us when we created the project. The implementation, testImplementation, and androidTestImplementation lines indicate libraries that we want to use, where implementation is for our app and the others are for our tests. Four of the five dependencies are ones that we will use, either currently or in the future. The one that we will not is: implementation fileTree(include: ['*.jar'], dir: 'libs')

This says “hey, look in the libs/ directory of this module, and pull in any JAR files you find in there”. We will not have any such JARs. So, you can delete the libs/ directory from the app/ module directory, by right-clicking over libs/ and choosing “Delete” from the context menu. Then, delete the implementation line shown above, so our build process does not try looking in that now-deleted directory. At this point, you should get a banner at the top of the editor, offering you the chance to “Sync Now”:

Figure 48: Android Studio “Sync Now” Banner Since we have other changes to make to this file, you can hold off on clicking that link.

Step #2: Upgrading Our SDK Versions Each app has three SDK version numbers of relevance: • the compileSdkVersion, which controls what classes, methods, and other symbols are available to us when we build the app; • the minSdkVersion, which specifies the oldest version of Android that we are willing to support; and

62


ADDING A LIBRARY • the targetSdkVersion, which means “this is the version of the Android SDK that I was thinking of when I wrote this code” and is used to help make our apps run better on future newer devices Right now, the compileSdkVersion and targetSdkVersion are 26, while the minSdkVersion is 21. The minSdkVersion is fine; some elements of this app will require Android 5.0 or higher. However, the compileSdkVersion and targetSdkVersion are lower than the latest API level published by Google. Google intends to force developers to continuously raise the targetSdkVersion. So, since we are starting from scratch (almost), we should use the now-current SDK version. So, change the compileSdkVersion and targetSdkVersion to 27: compileSdkVersion 27 defaultConfig { applicationId "com.commonsware.todo" minSdkVersion 21 targetSdkVersion 27 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" }

(from T05-Libraries/ToDo/app/build.gradle)

The “Sync Now” banner will still be there, tempting you to click its link. You can do so, but the banner will reappear in the next step, so you can just wait for now.

Step #3: Adding Support for RecyclerView The idea is that the ToDo app will present a list of tasks to be done. That requires that we have something to display a list to the user. There are two typical solutions for that problem: ListView and RecyclerView. RecyclerView is more modern and more flexible, so it is a good choice for this problem. However, ListView does have one advantage over RecyclerView: ListView is part of the framework portion of the Android SDK, and so it is always available to apps. RecyclerView requires us to add a library to the app. Fortunately, we happen to be in a tutorial where we are working with the dependencies in the app. RecyclerView

is part of the core set of Android Support Libraries. These all share a common version scheme. Moreover, we have to use the same version for all of them 63


ADDING A LIBRARY — otherwise, the app will fail with a build error. So, not only will we add the library for RecyclerView, but we will set things up to be able to share a common version number for all of the Support Libraries that we need to use. To that end, just before the dependencies closure, add the following line to app/ build.gradle: def supportVer="27.1.1" (from T05-Libraries/ToDo/app/build.gradle)

This defines a variable, named supportVer, in our build script. It specifies the version of the Support Libraries that we are going to use. Its major version matches our compileSdkVersion and targetSdkVersion, which is what the Support Libraries require. Then, inside the dependencies closure, add the following line: implementation "com.android.support:recyclerview-v7:$supportVer" (from T05-Libraries/ToDo/app/build.gradle)

Make sure that you use double quotes, not single quotes, for the string. Gradle is implemented in the Groovy scripting language, and Groovy supports “string interpolation” inside of double-quoted strings. Any values with a $ prefix will be interpreted as references to variables, and those values will be substituted into the string. This is how we get the version number from supportVer into the implementation line. At this point, go ahead and click the “Sync Now” link in the banner at the top of the editor. Your resulting app/build.gradle file should now resemble: apply plugin: 'com.android.application' android { compileSdkVersion 27 defaultConfig { applicationId "com.commonsware.todo" minSdkVersion 21 targetSdkVersion 27 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes {

64


ADDING A LIBRARY release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } def supportVer="27.1.1" dependencies { implementation "com.android.support:recyclerview-v7:$supportVer" implementation 'com.android.support.constraint:constraint-layout:1.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }

(from T05-Libraries/ToDo/app/build.gradle)

What We Changed The bookâ&#x20AC;&#x2122;s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: â&#x20AC;˘ app/build.gradle

65


Constructing a Layout

Our starter project has a layout resource: res/layout/activity_main.xml already. However, it is just a bit different from what we need. So, in this tutorial, we will modify that layout, using the Android Studio drag-and-drop GUI builder. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Read Me! This tutorial assumes that you have learned the basics of working with layout resources — particularly ConstraintLayout — elsewhere. That could be: • From the official documentation, such as their material on creating layouts • Several chapters from The Busy Coder’s Guide to Android Development, including: ◦ “The Theory of Widgets” ◦ “The Android User Interface” ◦ “Basic Widgets” ◦ “The Classic Container Classes” ◦ “Other Common Widgets and Containers” ◦ “GUI Building, Continued” ◦ “Introducing ConstraintLayout” ◦ “RecyclerView” • Other educational resources

67


CONSTRUCTING A LAYOUT

Step #1: Examining What We Have And What We Want The starter project has a single layout resource, in res/layout/activity_main.xml. If you open that up in the IDE and switch to the “Text” sub-tab, you will see XML like this: <?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout 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="com.commonsware.todo.MainActivity"> > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout>

We have a ConstraintLayout as our root container. ConstraintLayout comes from that com.android.support.constraint:constraint-layout artifact that we saw in our dependencies list in the preceding tutorial. ConstraintLayout is Google’s recommended base container for most layout resources, as it is the most flexible option. Inside, we have a TextView, with a simple “Hello World!” message. As it turns out, we can use both of those in the UI that we are going to construct:

68


CONSTRUCTING A LAYOUT

Figure 49: Android Studio Layout Designer, Showing End Result of This Tutorial We want: • a RecyclerView, to use for our list of to-do items • a TextView, to show when the RecyclerView is empty The RecyclerView and the TextView will go in the same space. In code, we will toggle the visibility of the TextView, so that it is visible when we have no to-do items to show in the RecyclerView and hidden when we have one or more to-do items to show.

Step #2: Adding a RecyclerView In the GUI builder, in the “Palette” area, switch to “Containers” category:

69


CONSTRUCTING A LAYOUT

Figure 50: Android Studio Layout Designer Palette Drag a RecyclerView out of the “Palette” and drop it roughly in the center of the preview area:

Figure 51: Android Studio Layout Designer, Showing Added RecyclerView

70


CONSTRUCTING A LAYOUT Unfortunately, the Android Studio Layout Designer has many issues, including making the RecyclerView too big to manipulate. Grab the upper-right corner of the RecyclerView and drag it inwards to shrink it a bit:

Figure 52: Android Studio Layout Designer, Showing Smaller RecyclerView Then drag the RecyclerView away from the left edge a bit, to give you room to maneuver:

71


CONSTRUCTING A LAYOUT

Figure 53: Android Studio Layout Designer, Showing Moved RecyclerView Hover your mouse over the left edge of the RecyclerView preview rectangle, find the “Create Connections” dot towards the center of the left edge, and drag it to connect with the left edge of the preview area, which will connect it to that side of the ConstraintLayout:

72


CONSTRUCTING A LAYOUT

Figure 54: Android Studio Layout Designer, Showing RecyclerView Anchored on the Left Repeat that process on the right side:

73


CONSTRUCTING A LAYOUT

Figure 55: Android Studio Layout Designer, Showing RecyclerView Anchored on Both Sides Repeat that process on the top side:

74


CONSTRUCTING A LAYOUT

Figure 56: Android Studio Layout Designer, Showing RecyclerView Anchored on Both Sides and the Top Repeat that process on the bottom side:

75


CONSTRUCTING A LAYOUT

Figure 57: Android Studio Layout Designer, Showing RecyclerView Anchored on All Four Sides In the “Attributes” pane on the right side of the Layout Designer, change the layout_width and layout_height values each to match_constraint (a.k.a., 0dp):

Figure 58: Android Studio Layout Designer, Attributes Pane, Showing New Sizes Now, you should see our RecyclerView fill the entire space:

76


CONSTRUCTING A LAYOUT

Figure 59: Android Studio Layout Designer, Showing RecyclerView Filling the Screen Back in the “Attributes” pane, give the RecyclerView an ID of items, via the field at the top. In the diagram beneath the ID field, change the 8 values to 0, by clicking on the 8, then choosing 0 from the drop-down list that appears:

Figure 60: Android Studio Layout Designer, Attributes Pane, Showing Margin DropDown

77


CONSTRUCTING A LAYOUT The drag-and-drop process automatically sets up some margins, but we will be applying margins elsewhere (in the RecyclerView rows) and so we do not need it here.

Step #3: Adjusting the TextView We can reuse the TextView that came in the starter project, but we need to make a few changes to it. However, to change it, we need to select it first, and now it is covered by the RecyclerView that we just added. Instead, click on the TextView entry in the “Component Tree” pane of the Layout Designer:

Figure 61: Android Studio Layout Designer, Component Tree Pane Then, in the “Attributes” pane, fill in empty for the ID. Then, click on the “…” button to the side of the “text” field that has “Hello World!” as its current value:

Figure 62: Android Studio Layout Designer, Attributes Pane, with Arrow Pointing to Button 78


CONSTRUCTING A LAYOUT This will bring up a dialog showing available string resources:

Figure 63: Android Studio String Resource Selector Click the “Add new resource” drop-down towards the top, and in there choose “New string Value”. This brings up a dialog to define a new string resource:

79


CONSTRUCTING A LAYOUT

Figure 64: Android Studio New String Resource Dialog For the “Resource name”, fill in msg_empty. For the “Resource value”, fill in “placeholder text”:

80


CONSTRUCTING A LAYOUT

Figure 65: Android Studio New String Resource Dialog, with Values As the text suggests, this is a placeholder for a better message that we will swap in later in this book. Click OK to define the resource, and you should be taken back to the designer, where you will see the new text:

81


CONSTRUCTING A LAYOUT

Figure 66: Android Studio Layout Designer, Showing Revised TextView Content Switch back to the “Text” subtab of the editor, where you can see the XML of the layout. Add android:textAppearance="?android:attr/textAppearanceMedium" as an attribute to the <TextView> element: <TextView android:id="@+id/empty" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/msg_empty" android:textAppearance="?android:attr/textAppearanceMedium" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> (from T06-Layout/ToDo/app/src/main/res/layout/activity_main.xml)

This says “we want this text to be in the standard medium text size for whatever overall UI theme we happen to be using”. Unfortunately, there is no good way to set that up in the “Attributes” pane of the designer, so we are stuck adding it through the XML.

82


CONSTRUCTING A LAYOUT This, then, gives us what we were seeking from the outset: the RecyclerView, and the TextView, all properly configured and positioned:

Figure 67: Android Studio Layout Designer, Showing End Result of This Tutorial <?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout 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="com.commonsware.todo.MainActivity"> > <TextView android:id="@+id/empty" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/msg_empty" android:textAppearance="?android:attr/textAppearanceMedium" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <android.support.v7.widget.RecyclerView android:id="@+id/items" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />

83


CONSTRUCTING A LAYOUT </android.support.constraint.ConstraintLayout>

(from T06-Layout/ToDo/app/src/main/res/layout/activity_main.xml)

If you run the app, since the MainActivity loads up this layout resource via setContentView(R.layout.activity_main), you will see the “placeholder text” and nothing else:

Figure 68: ToDo App, As Currently Implemented We have not put anything into the RecyclerView, so it has no content for us to see.

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • app/src/main/res/layout/activity_main.xml

84


Setting Up the Action Bar

Next up is to configure the action bar to our ToDo application. The action bar is that bar at the top of your activity UI, showing your app’s title. It can also have toolbarstyle buttons and an “overflow menu”, each holding what are known as action items. In this tutorial, we will add an action bar item to launch an “about” page, though we will not actually show that page until a later tutorial. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial. Starting in this tutorial, we will begin editing Java source files. Some useful Android Studio shortcut key combinations are: Alt-Enter ( Option-Return on macOS) for bringing up quick-fixes for the problem at the code where the cursor is. • Ctrl-Alt-O ( Command-Option-O on macOS) will organize your Java import statements, including removing unused imports. • Ctrl-Alt-L ( Command-Option-L on macOS) will reformat the Java or XML in the current editing window, in accordance with either the default styles in Android Studio or whatever you have modified them to in Settings. •

NOTE: Copying and pasting Java code from this book may or may not work, depending on what you are using to read the book. For the PDF, some PDF viewers (e.g., Adobe Reader) should copy the code fairly well; others may do a much worse job. Reformatting the code with Ctrl-Alt-L ( Command-Option-L on macOS) after pasting it in sometimes helps.

85


SETTING UP THE ACTION BAR

Read Me! This tutorial assumes that you have learned the basics of the action bar elsewhere. That could be: • From the official documentation, such as their limited material on menus • The Busy Coder’s Guide to Android Development and its “The Action Bar” chapter and its “Vector Drawables” chapter • Other educational resources

Step #1: Adding an Icon We are going to need a an icon for our action bar item. Nowadays, the preferred approach for doing this is to start with vector drawables, rather than bitmaps, to reduce the size of the app and maximize the quality of the icons when they are displayed. Right-click over the res/ directory and choose New > “Vector Asset” from the context menu. This brings up the first page of the vector asset wizard:

Figure 69: Android Studio Vector Asset Studio, As Initially Launched

86


SETTING UP THE ACTION BAR Click on the Icon button. This will bring up an icon selector, with a bunch of icons from Google’s “Material Design” art library. In the search field, type info, then click on the “info outline” icon:

Figure 70: Android Studio Icon Selector, Showing “info outline” Icon Click “OK”. This will update the name of the asset to ic_info_outline_black_24dp. Click Next, then Finish, to add that icon as an XML file in res/drawable/.

Step #2: Defining an Item Next, we will add a low-priority action item, for an “about” screen. Right click over the res/ directory in your project, and choose New > “Android resource directory” from the context menu. This will bring up a dialog to let you create a new resource directory:

87


SETTING UP THE ACTION BAR

Figure 71: Android Studio New Resource Directory Dialog Change the “Resource type” drop-down to be “menu”, then click OK to create the directory. Then, right-click over your new res/menu/ directory and choose New > “Menu resource file” from the context menu. Fill in actions.xml in the “New Menu Resource File” dialog:

Figure 72: Android Studio New Menu Resource File Dialog Then click OK to create the file. It will open up into a menu editor:

88


SETTING UP THE ACTION BAR

Figure 73: Android Studio Menu Designer In the Palette, drag a “Menu Item” into the preview area. This will appear as an item in an overflow area:

89


SETTING UP THE ACTION BAR

Figure 74: Android Studio Menu Designer, with a New Menu Item In the Attributes pane, fill in: • about for the “id” • “never” for “showAsAction”

Figure 75: Android Studio Menu Designer, Attributes Pane, Showing “showAsAction” Options 90


SETTING UP THE ACTION BAR Click on the “…” button next to the “icon” field. This will bring up a drawable resource selector:

Figure 76: Android Studio Drawable Resource Selection Dialog Initially, this may appear with all categories on the left in a collapsed state, as shown in the above screenshot. Fold open the “Project” category to expose all drawables defined locally within the project:

91


SETTING UP THE ACTION BAR

Figure 77: Android Studio Drawable Resource Selection Dialog, Showing Project Resources Click on ic_info_outline_black_24dp in the list of drawables, then click OK to accept that choice of icon. In truth, this is unnecessary, as our item should never show the icon. But, you never know when someday Google will decide to show icons for overflow menu items, so it is best to define one. Then, click the “…” button next to the “title” field. As before, this brings up a string resource selector. Click on “Add new resource” > “New string Value” in the dropdown towards the top. In the dialog, fill in menu_about as the resource name and “About” as the resource value:

92


SETTING UP THE ACTION BAR

Figure 78: Android Studio New String Resource Dialog Click OK to close the dialog, and you will see your new title appear in the menu editor:

93


SETTING UP THE ACTION BAR

Figure 79: Android Studio Menu Designer, Showing About Item

Step #3: Loading and Responding to Our Options Simply defining res/menu/actions.xml is insufficient. We need to actually tell Android to use what we defined in that file, and we need to add code to respond to when the user taps on our items. Go into the project tree, and drill down into the java/ directory to find the com.commonsware.todo package and the MainActivity class inside of it:

Figure 80: Android Studio Project Tree, Showing MainActivity 94


SETTING UP THE ACTION BAR Double-click on MainActivity to open it in an editor. There, add an onCreateOptionsMenu() method and an onOptionsItemSelected() method to MainActivity: @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.actions, menu); return super super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId()==R.id.about) { return true true; } return super super.onOptionsItemSelected(item); } (from T07-ActionBar/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

In onCreateOptionsMenu(), we are inflating res/menu/actions.xml and pouring its contents into the supplied Menu object, which will be used by Android to populate our action bar. In onOptionsItemSelected(), weâ&#x20AC;Ś do nothing, as we have nothing to show at the moment. If you copied and pasted this code from the book, you should get a bunch of errors:

95


SETTING UP THE ACTION BAR

Figure 81: Android Studio Java Editor, Showing Errors That is because we are missing two import statements, for Menu and MenuItem: import android.view.Menu android.view.Menu; import android.view.MenuItem android.view.MenuItem; (from T07-ActionBar/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

You can add those to the top of the file yourself. Or, in Android Studio, put the text cursor inside one of the missing classes (e.g., Menu) and press Alt-Enter ( Command-Return on macOS) to bring up the “quick fix” menu for the problem:

Figure 82: Android Studio “Quick Fix” Menu Choose “Import Class” from the menu to have Android Studio add the import statement for you.

96


SETTING UP THE ACTION BAR

Step #5: Trying It Out If you run the app, you should see a “…” icon on the action bar:

Figure 83: ToDo App, Showing Overflow Menu Affordance Pressing that brings up a menu showing our “About” item:

97


SETTING UP THE ACTION BAR

Figure 84: ToDo App, Showing Overflow Menu with About Item Tapping that item has no effect â&#x20AC;&#x201D; we will address that in an upcoming tutorial.

Step #6: Dealing with Crashes Most likely, you will not need this step. But, sometimes, when writing Android apps, you will make mistakes. Your code will compile, but then it will crash at runtime. A crash is signaled by a dialog indicating that there was a problem. The look of that dialog varies by Android version, but a typical one is:

98


SETTING UP THE ACTION BAR

Figure 85: Crash Dialog, on Android 8.1 When that occurs, you can find out more about the crash by opening the LogCat tool in Android Studio. By default, this is docked along the lower edge. Opening it gives you access to all sorts of messages logged by apps and the operating system. There will be lots of messages. Ideally, Android Studio would help you narrow down the messages. It offers a couple of things for that: • There is a message “severity” drop down (third from left in the screenshot below), showing options like “Verbose” and “Error” — crashes are logged at “Error” severity • The end drop-down will default to “Show only selected application”, which will then (theoretically) limit the output to only messages logged by your app, or by whatever app is shown in the second drop-down

Figure 86: LogCat, Showing Stack Trace

99


SETTING UP THE ACTION BAR When you crash, you will get a red Java stack trace showing what went wrong: 05-15 19:53:27.651 8937-8937/com.commonsware.todo E/AndroidRuntime: FATAL EXCEPTION: main Process: com.commonsware.todo, PID: 8937 android.content.res.Resources$NotFoundException: Resource ID #0x7f060000 type #0x12 is not valid at android.content.res.Resources.loadXmlResourceParser(Resources.java:2139) at android.content.res.Resources.getLayout(Resources.java:1143) at android.view.MenuInflater.inflate(MenuInflater.java:111) at com.commonsware.todo.MainActivity.onCreateOptionsMenu(MainActivity.java:18) at android.app.Activity.onCreatePanelMenu(Activity.java:3388) at com.android.internal.policy.PhoneWindow.preparePanel(PhoneWindow.java:631) at com.android.internal.policy.PhoneWindow.doInvalidatePanelMenu(PhoneWindow.java:1024) at com.android.internal.policy.PhoneWindow$1.run(PhoneWindow.java:264) at android.os.Handler.handleCallback(Handler.java:790) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6494) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

In this case, this comes from a modified version of this sample app, hacked to introduce a crash. Typically, you look for the top-most line that refers to your code. In this case, that is: at com.commonsware.todo.MainActivity.onCreateOptionsMenu(MainActivity.java:18)

The location (MainActivity.java:18) will be a link that you can click to jump to that particular line of code. That, plus the error message, will hopefully help you diagnose exactly what went wrong.

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • app/src/main/res/menu/actions.xml • app/src/main/java/com/commonsware/todo/MainActivity.java 100


Customizing Our Theme

If you look at our activity in a device or emulator, you will notice that the action bar is dark gray. There is nothing wrong with dark gray as a color. However, it is very boring. When you run other apps on that same device or emulator, you will see that many of them use some other shade for their action bar color. Frequently, it is a color that is tied to their branding. Other times, it is just a color that the developer or designer liked. In most cases, it is not dark gray. The action bar color is one aspect of our app that is managed by a theme. A theme provides overall “look and feel” instructions for our activity, including the action bar color. So, in this tutorial, we will set up a custom theme, where we can use colors that we prefer. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Read Me! This tutorial assumes that you have learned the basics of themes elsewhere. That could be: • From the official documentation, such as their material on styles and themes • The Busy Coder’s Guide to Android Development and its “Defining and Using Styles” chapter • Other educational resources

101


CUSTOMIZING OUR THEME

Step #1: Defining Some Colors Just as Android has layout and menu and string resources, Android has color resources. We can define some colors in a resource file, then apply those colors elsewhere in our app. By convention, colors are defined in a colors.xml file. Colors are considered “value” resources, like our strings, and so the file would go into res/values/colors.xml. But, we need to choose some colors. To that end, visit https://www.materialpalette.com/, which offers a very simple point-and-click way of setting up a color palette for use in an Android app:

Figure 87: Material Design Palette Site, As Initially Launched For the purposes of this tutorial, click on “Yellow”, then “Light Blue”:

102


CUSTOMIZING OUR THEME

Figure 88: Material Design Palette Site, With Yellow/Light Blue Colors The site assumes that the action bar will be dark with light text, but that is merely a limitation of the site. We will teach Android to use dark text, and so the white-onyellow effect seen here is not going to be a problem. Then, click the “Download” button in the “Your Palette” area, and choose “XML” as the type of file to download. This will trigger your browser to download a file named colors_yellow_light_blue.xml. Move it into res/values/ of your module, and then open it in the IDE. You should see: <!-- Palette generated by Material Palette - materialpalette.com/yellow/light-blue --> <?xml version="1.0" encoding="utf-8"?> <resources> <color name="primary"> >#FFEB3B</color> </color> <color name="primary_dark"> >#FBC02D</color> </color> <color name="primary_light"> >#FFF9C4</color> </color> <color name="accent"> >#03A9F4</color> </color> <color name="primary_text"> >#212121</color> </color> <color name="secondary_text"> >#757575</color> </color> <color name="icons"> >#212121</color> </color> <color name="divider"> >#BDBDBD</color> </color> </resources>

103


CUSTOMIZING OUR THEME Unfortunately, the site adds a comment at the top of the XML file, which is not a legal location, and so Android Studio will complain about the comment. Delete it. You will see that the editor contains color swatches in the â&#x20AC;&#x153;gutterâ&#x20AC;? area, adjacent to each of the color values:

Figure 89: Android Studio Values Resource Editor, with Color Swatches The color swatches are clickable and will bring up a color picker, if you wanted to change any of the colors a bit from what the site gave you:

Figure 90: Android Studio Color Picker

104


CUSTOMIZING OUR THEME For the purposes of this tutorial, leave the colors alone.

Step #2: Configuring the Custom Theme Your project already has a custom theme declared. If you look in your res/values/ directory, you will see a styles.xml file — open that in Android Studio. By default, it will open up in a regular XML editor: <resources>

<!-- Base application theme. --> <style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar"> > <!-- Customize your theme here. --> </style> </resources>

Here, we see that we have a style resource named AppTheme. Style resources can be applied either to widgets (to tailor that particular widget) or as a theme to an activity or entire application. By convention, style resources with “Theme” in the name are themes. This particular theme inherits from Theme.Material.Light.DarkActionBar, as indicated in the parent attribute. However, it does not change anything that it inherits, so the look-and-feel of our activity is the same as if we used Theme.Material.Light.DarkActionBar directly. The first problem is that we will be using a light action bar, not a dark one. So, remove the DarkActionBar portion of that parent value, leaving you with Theme.Material.Light. Then, replace the “Customize your theme here.” comment with three <item> elements to tie those colors into our theme: <item name="android:colorPrimary"> >@color/primary</item> </item> <item name="android:colorPrimaryDark"> >@color/primary_dark</item> </item> <item name="android:colorAccent"> >@color/accent</item> </item>

The three theme attributes that we are defining are: • colorPrimary, which will be the color of the action bar • colorPrimaryDark, which will be the color of the status bar (the bar that appears at the very top of the screen, showing the time, battery level, WiFi signal strength, etc.) 105


CUSTOMIZING OUR THEME â&#x20AC;˘ colorAccent, which will be used for certain pieces of widgets, such as the text-selection cursor in EditText widgets The resulting resource should look like: <resources>

<!-- Base application theme. --> <style name="AppTheme" parent="android:Theme.Material.Light"> > <item name="android:colorPrimary"> >@color/primary</item> </item> <item name="android:colorPrimaryDark"> >@color/primary_dark</item> </item> <item name="android:colorAccent"> >@color/accent</item> </item> </style> </resources> (from T08-Theme/ToDo/app/src/main/res/values/styles.xml)

The color swatches in the gutter here are non-clickable, so you cannot edit the colors from the style resource.

Step #3: Trying It Out Our AndroidManifest.xml file already ties in this custom theme, via the android:theme attribute in the <application> element: <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:tools="http://schemas.android.com/tools" package="com.commonsware.todo" xmlns:android="http://schemas.android.com/apk/res/android"> > <supports-screens android:largeScreens="true" android:normalScreens="true" android:smallScreens="true" android:xlargeScreens="true" /> <application android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> >

106


CUSTOMIZING OUR THEME <activity android:name=".MainActivity"> > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> (from T08-Theme/ToDo/app/src/main/AndroidManifest.xml)

As a result, if you run your app on a device or emulator, you will see the primary and â&#x20AC;&#x153;primary darkâ&#x20AC;? colors applied to the action bar and status bar, respectively:

Figure 91: ToDo App, with New Theme The accent color will show up in later tutorials, as we start adding more to our user interface.

107


CUSTOMIZING OUR THEME

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • app/src/main/res/values/colors_yellow_light_blue.xml • app/src/main/res/values/styles.xml

108


Setting Up an Activity

Of course, it would be nice if that “About” menu item that we added in a previous tutorial actually did something. In this tutorial, we will define another activity class, one that will be responsible for the “about” details. And, we will arrange to start up that activity when that menu item is selected. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Read Me! This tutorial assumes that you have learned the basics of starting up activities from somewhere. That could be: • From the official documentation, such as their material on the activity lifecycle • The Busy Coder’s Guide to Android Development and its “Activities and Their Lifecycles” chapter • Other educational resources

Step #1: Creating the Stub Activity Class and Manifest Entry First, we need to define the Java class for our new activity, AboutActivity.

109


SETTING UP AN ACTIVITY Right-click on your main/ source set directory in the project explorer, and choose New > Activity > Empty Activity from the context menu. This will bring up a newactivity wizard:

Figure 92: Android Studio New-Activity Wizard, As Initially Launched Fill in AboutActivity in the “Activity Name” field. Leave “Launcher Activity” unchecked, and uncheck the “Backwards Compatibility (AppCompat)” checkbox. If the package name drop-down is showing the app’s package name (com.commonsware.todo), leave it alone. On the other hand, if the package name drop-down is empty, click on it and choose the app’s package name. Leave the source language drop-down set to Java.

110


SETTING UP AN ACTIVITY

Figure 93: Android Studio New-Activity Wizard, Filled In If you click on Finish, Android Studio will create your AboutActivity class and open it in the editor. The source code should look like: package com.commonsware.todo; import android.app.Activity android.app.Activity; import android.os.Bundle android.os.Bundle; public class AboutActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super super.onCreate(savedInstanceState); setContentView(R.layout.activity_about); } }

The new-activity wizard also added a manifest entry for us: <activity android:name=".AboutActivity"></activity> ></activity> (from T09-Activities/ToDo/app/src/main/AndroidManifest.xml)

111


SETTING UP AN ACTIVITY

Step #2: Adding a WebView In addition to a new AboutActivity Java class and manifest entry, the new-activity wizard created an activity_about layout resource for us, alongside the existing activity_main layout. Open activity_about into the graphical layout editor, and note that the designer now shows our custom theme. In the “Palette”, choose the “Widgets” category, and drag a WebView into the preview area:

Figure 94: Android Studio Layout Designer, Showing WebView However, while the WebView might seem like it is set to fill all of the available space, the design tool probably just assigned it some hard-coded values, ones that make it difficult to work with. So, in the “Attributes” pane, temporarily assign wrap_content to both “layout_width” and “layout_height”, to give you a smaller WebView to work with:

112


SETTING UP AN ACTIVITY

Figure 95: Android Studio Layout Designer, Showing Smaller WebView Then, as we did with the RecyclerView back in the layout tutorial, drag the circles from the sides of the WebView and attach them to the corresponding sides of the ConstraintLayout:

113


SETTING UP AN ACTIVITY

Figure 96: Android Studio Layout Designer, Showing Constrained WebView Then, go back to the “Attributes” pane and set the “layout_width” and “layout_height” each to match_constraint (a.k.a., 0dp), to have the WebView fill all of the available space:

114


SETTING UP AN ACTIVITY

Figure 97: Android Studio Layout Designer, Showing Full-Screen WebView Also, back in the “Attributes” pane, give the WebView an “ID” of about.

Step #3: Launching Our Activity Now that we have declared that the activity exists and can be used, we can start using it. Go into MainActivity and modify onOptionsItemSelected() to start AboutActivity if the user chooses the about menu item: @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId()==R.id.about) { startActivity(new new Intent(this this, AboutActivity.class)); return true true; } return super super.onOptionsItemSelected(item); } (from T09-Activities/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

115


SETTING UP AN ACTIVITY Here, we create an Intent, pointing at our new AboutActivity. Then, we call startActivity() on that Intent. You will need to add an import for android.content.Intent to get this to compile. If you run this app in a device or emulator, and you choose the About overflow item… nothing much appears to happen, other than our “placeholder text” vanishes. In reality, what happens is that our AboutActivity appeared, but empty, as we have not given the WebView any content yet.

Step #4: Defining Some About Text We need some HTML to put into the WebView. We could load some from the Internet. However, then the user can only view the about text when they are online, which seems like a silly requirement. Instead, we can package some HTML as an asset inside of our app, then display that HTML in the WebView. To that end, right-click over the main source set directory and choose “New” > “Directory” from the context menu. That will pop up a dialog, asking for the name of the directory to create:

Figure 98: Android Studio New Directory Dialog Fill in assets and click “OK” to create this directory. Then, right-click over your new assets/ directory and choose “New” > “File” from the context menu. Once again, you will get a dialog, this time to provide the filename. Fill in about.html and click “OK” to create this file. It should also open up an editor tab on that file, which will be empty. There, fill in some HTML. For example, you could use: <h1> <h1>About This App</h1> </h1> <p> <p>This app is cool!</p> </p> <p> <p>No, really &mdash; this app is awesome!</p> </p>

116


SETTING UP AN ACTIVITY <div> . <br/> . <br/> . <br/> . </div> <p> <p>OK, this app isn't all that much. But, hey, it's mine!</p> </p> (from T09-Activities/ToDo/app/src/main/assets/about.html)

Step #5: Populating the WebView Open up AboutActivity into the editor, and change it to: package com.commonsware.todo; import android.app.Activity android.app.Activity; import android.os.Bundle android.os.Bundle; import android.webkit.WebView android.webkit.WebView; public class AboutActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super super.onCreate(savedInstanceState); setContentView(R.layout.activity_about); WebView wv=findViewById(R.id.about); wv.loadUrl("file:///android_asset/about.html"); } } (from T09-Activities/ToDo/app/src/main/java/com/commonsware/todo/AboutActivity.java)

Here, we retrieve the about WebView from our inflated layout, then call loadUrl() on it to tell it what to display. loadUrl() normally takes an https URL, but in this case, we use the special file:///android_asset/ notation to indicate that we want to load an asset out of assets/. file:///android_asset/ points to the root of assets/, so file:///android_asset/about.html points to assets/about.html.

117


SETTING UP AN ACTIVITY (yes, file:///android_asset/ is singular, and assets/ is plural – eventually, you just get used to this…) If you now run the app, and choose “About” from the overflow, you will see your about text:

Figure 99: ToDo About Activity

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • • • • •

app/src/main/AndroidManifest.xml app/src/main/res/layout/activity_about.xml app/src/main/assets/about.html app/src/main/java/com/commonsware/todo/MainActivity.java app/src/main/java/com/commonsware/todo/AboutActivity.java

118


Integrating Fragments

As we saw at the outset, there will be three main elements of the user interface when we are done: • a list of to-do items • a place to edit an item, whether that is a new one being added to the list or modifying an existing one • a place to view details of a single item We could implement all of those as activities, if we wanted to. However, that will make it difficult to implement a good UI on a tablet-sized device. Each one of those three elements is much too small to be worth taking up an entire 10" tablet screen, for example. So, while we will show one of those elements at a time on smaller screens, on larger screens we will show two at a time: • the list plus the details, or • the list plus the editor We cannot do this with three independent activities. This is where fragments come into play. We can define each of the three elements as a fragment, then arrange to show either one or two fragments at a time, based upon screen size. In this chapter, we will convert our existing list into a fragment and have our activity display that fragment. This will have no immediate impact upon the user experience — the app will be unchanged visibly as a result of these changes. But, we will be setting ourselves up for creating the other two elements — a details fragment and an edit fragment — in later tutorials. And while we will focus on smaller-screen devices now, in a later tutorial we can adapt our app to show two fragments at a time on larger-screen devices.

119


INTEGRATING FRAGMENTS This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Read Me! This tutorial assumes that you have learned the basics of working with fragments from somewhere. That could be: • From the official documentation, such as their material on fragments • The Busy Coder’s Guide to Android Development and its “The Tactics of Fragments” chapter • Other educational resources

Step #1: Adding the Requisite Library There are two implementations of fragments available to Android developers: • the native implementation (android.app.Fragment), which was added in API Level 11 • the support library implementation (android.support.v4.app.Fragment) Since our minSdkVersion is 21, we could use the native fragments if desired. However, there are some advantages for using the support library implementation. Chief among these is that bug fixes get published in the form of library updates. So, for example, any bugs in Android 5.0’s edition of fragments are still bugs in Android 5.0 devices, but those bugs (hopefully) have been fixed in up-to-date versions of the support library. So, these tutorials will use the support library implementation of fragments. That will require another library, so add this line to your dependencies closure of your app/build.gradle file: implementation "com.android.support:support-fragment:$supportVer" (from T10-Fragments/ToDo/app/build.gradle)

The entire dependencies closure should now look like: def supportVer="27.1.1"

120


INTEGRATING FRAGMENTS dependencies { implementation "com.android.support:recyclerview-v7:$supportVer" implementation "com.android.support:support-fragment:$supportVer" implementation 'com.android.support.constraint:constraint-layout:1.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }

(from T10-Fragments/ToDo/app/build.gradle)

Android Studio will want to sync Gradle with the project build files â&#x20AC;&#x201D; go ahead and let it do that.

Step #2: Migrating to FragmentActivity One limitation of the support library implementation of fragments is that we have to inherit from a FragmentActivity (supplied by the library) instead of the Activity that we have been using for our activities. FragmentActivity itself inherits from Activity, but it adds in code that works with the support library fragment implementation. Open MainActivity in Android Studio, and change it to have extends FragmentActivity instead of extends Activity: public class MainActivity extends FragmentActivity { (from T10-Fragments/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

Note that this will also require a change in import statements, from import android.app.Activity to import android.support.v4.app.FragmentActivity. Then, repeat that same change for AboutActivity: public class AboutActivity extends FragmentActivity { (from T10-Fragments/ToDo/app/src/main/java/com/commonsware/todo/AboutActivity.java)

Step #3: Creating a Fragment Next, we need to set up a fragment. While Android Studio offers a new-fragment wizard, its results are awful, so we will create one as a normal Java class.

121


INTEGRATING FRAGMENTS Right-click over the com.commonsware.todo package in the java/ directory and choose “New” > “Java Class” from the context menu. This will bring up a dialog where we can define a new Java class:

Figure 100: Android Studio Create Java Class Dialog For the name, fill in RosterListFragment, as this fragment is showing a list of our roster of to-do items. For the superclass, fill in android.support.v4.app.Fragment. The simplest way to do this is to just start typing in Fragment and let the auto-complete suggest the proper class for you:

122


INTEGRATING FRAGMENTS

Figure 101: Android Studio Create Java Class Dialog, Filled In Then, click OK to create the class. That will give you a RosterListFragment that looks like: package com.commonsware.todo; import android.support.v4.app.Fragment android.support.v4.app.Fragment;

/** * Created by ... on ... */ public class RosterListFragment extends Fragment { }

(where the ... values in the comment will vary based on your user ID on your development machine and todayâ&#x20AC;&#x2122;s date â&#x20AC;&#x201D; future code listings shown in these tutorials will skip this comment for brevity) However, this fragment does not do anything, and we need it to display our user interface. So, with your cursor inside the { } of the class, press Ctrl-O (or Command-O on macOS), to bring up a list of methods that we could override:

123


INTEGRATING FRAGMENTS

Figure 102: Android Studio Method Override Dialog If you start typing with that dialog on the screen, what you type in works as a search mechanism, jumping you to the first method that resembles what you typed in. So, start typing in onCreateView, until that becomes the selected method:

Figure 103: Android Studio Method Override Dialog, During Search Then, click OK to add a stub implementation of that method to your RosterListFragment: 124


INTEGRATING FRAGMENTS package com.commonsware.todo; import import import import import import

android.os.Bundle android.os.Bundle; android.support.annotation.Nullable android.support.annotation.Nullable; android.support.v4.app.Fragment android.support.v4.app.Fragment; android.view.LayoutInflater android.view.LayoutInflater; android.view.View android.view.View; android.view.ViewGroup android.view.ViewGroup;

public class RosterListFragment extends Fragment { @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return super super.onCreateView(inflater, container, savedInstanceState); } }

The @Override annotation means that we are overriding an existing method, and the @Nullable annotations mean that those parameters, and the return value, could be null. The job of onCreateView() of a fragment is to set up the UI for that fragment. In MainActivity, right now, we are doing that by calling setContentView(R.layout.activity_main). We want to use that layout file here instead. To do that, modify onCreateView() to look like: package com.commonsware.todo; import import import import import import

android.os.Bundle android.os.Bundle; android.support.annotation.Nullable android.support.annotation.Nullable; android.support.v4.app.Fragment android.support.v4.app.Fragment; android.view.LayoutInflater android.view.LayoutInflater; android.view.View android.view.View; android.view.ViewGroup android.view.ViewGroup;

public class RosterListFragment extends Fragment { @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.activity_main, container, false false); } }

125


INTEGRATING FRAGMENTS Here, we use the supplied LayoutInflater. To “inflate” in Android means “convert an XML resource into a corresponding tree of Java objects”. LayoutInflater inflates layout resources, via its family of inflate() methods. We are specifically saying: • Inflate R.layout.activity_main • Its widgets will eventually go into the container supplied to onCreateView() • Do not put those widgets in that container right now, as the fragment system will handle that for us at an appropriate time

Step #4: Displaying the Fragment Just because we have a fragment class does not mean that it will be displayed anywhere. We have to arrange to have that happen. One way to do this is to use a FragmentTransaction to modify the mix of fragments used in an activity. To that end, in MainActivity, modify onCreate() to look like this: @Override protected void onCreate(Bundle savedInstanceState) { super super.onCreate(savedInstanceState); if (getSupportFragmentManager().findFragmentById(android.R.id.content)==null null) { getSupportFragmentManager().beginTransaction() .add(android.R.id.content, new RosterListFragment()) .commit(); } }

(from T10-Fragments/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

This short block is fairly complex. First, we are using a FragmentManager, obtained by calling getSupportFragmentManager(), as we are using the support library edition of fragments, not the native ones. If you see getFragmentManager() used in apps, that is an app that is using the native fragments, not the support ones. We are asking the FragmentManager to give us the fragment located in a container identified as android.R.id.content, by calling findFragmentById() and passing in that container ID. This container is provided to every activity by the framework. When we called setContentView() before, we were telling the activity to take the contents of that layout resource and put it into this container. However, we can also use the container with fragments.

126


INTEGRATING FRAGMENTS findFragmentById()

will return an existing fragment in that container if there is one, otherwise it will return null. At the outset, that container will not have any fragments, so we would expect a null value back. If findFragmentById() returns null, we: • Ask the FragmentManager to begin a FragmentTransaction (via beginTransaction()) • Add a new instance of RosterListFragment to the android.R.id.content container via the add() method • “Commit” this transaction (via commit()), to cause its changes to appear on the screen So now we have migrated our minimal UI from being managed by the activity to being managed by a fragment, which in turn is managed by the activity.

Step #5: Renaming Our Layout Resource However, our layout resource now has a silly name. It is called activity_main, and it is not being displayed (directly) by an activity. Moreover, eventually, our MainActivity will be using three fragments, each with its own layout resource. So, let’s rename this layout to todo_roster instead. To do that, right-click over res/layout/activity_main.xml in the project tree, then choose “Refactor” > “Rename” from the context menu. This will bring up a dialog for you to provide the replacement name:

Figure 104: Android Studio Rename Dialog Change that to be todo_roster.xml, then click “Refactor”. Not only will this change the name of the file, but if you look at RosterListFragment, you will see that Android Studio also fixed up our inflate() call to use the new resource name:

127


INTEGRATING FRAGMENTS public class RosterListFragment extends Fragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.todo_roster, container, false false); } } (from T10-Fragments/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

Frequently, though not always, Android Studio will not only rename resources but also alter references to those resources to use the new name. If you run the app, everything looks as it did before, even though now our UI is managed by the fragment.

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • • • •

app/src/main/res/layout/todo_roster.xml app/src/main/java/com/commonsware/todo/MainActivity.java app/src/main/java/com/commonsware/todo/AboutActivity.java app/src/main/java/com/commonsware/todo/RosterListFragment.java

128


Defining a Model

If we are going to show to-do items in this list, it would help to have some to-do items. That, in turn, means that we need a Java class that represents a to-do item. Such a class is often referred to as a “model” class, so in this chapter, we will create a ToDoModel, where each ToDoModel instance represents one to-do item. However, we will not just make a simple Java class for this. Instead, we will make one that is immutable. Immutable classes are ones where you can create instances but cannot modify existing instances. For many Java classes, such as our MainActivity, having it be immutable is impossible or undesirable. However, for models, immutability has some benefits, particularly for helping us keep track of the state of the model (e.g., do we need to save it to the database?). Java does not have a way of making classes be immutable — unlike Kotlin, which does — but through Google’s AutoValue library, we can craft a model that, in practice, is immutable. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Read Me! This tutorial assumes that you have learned the use of Google’s AutoValue library from somewhere. That could be: • From the official AutoValue documentation • Android’s Architecture Components and its “Immutability” chapter • Other educational resources

129


DEFINING A MODEL

Step #1: Adding a Stub POJO First, let’s create the ToDoModel class. To do this, right-click over the com.commonsware.todo package in the project tree in Android Studio, and choose “New” > “Java Class” from the context menu. As before, this brings up a dialog where we can define a new Java class, by default into the same Java package that we rightclicked over. Fill in ToDoModel in the “Name” field and choose “Abstract” in the “Modifiers” set of radio buttons. Leave the rest of the dialog alone, and click “OK” to create this class. ToDoModel should show up in an editor, with an implementation like this: package com.commonsware.todo; public abstract class ToDoModel { }

Step #2: Linking to AutoValue For immutability with ToDoModel — and other classes we will add later — we are going to use Google’s AutoValue library. To add that library, we need to make some changes to the app/build.gradle file. There are two new dependencies that we will need to add. One is an “annotation processor”. In Java, those @-prefixed values that you see sprinkled throughout code are called “annotations”. Some, like @Override, are handled directly by the Java compiler. Others can be supported by third-party annotation processors, which can generate “behind the scenes” Java code for you based on those annotations. The other dependency is a library that defines the annotations that the annotation processor will use. These two dependencies are linked, and we need to use matching versions. So just as we used supportVer to have a consistent version number for our Support Library artifacts, let’s define an autoValueVer value that has the version number of AutoValue. So, under the def supportVer statement in app/build.gradle, add: def autoValueVer="1.5.1" (from T11-Model/ToDo/app/build.gradle)

Then, in the list of dependencies, add:

130


DEFINING A MODEL compileOnly "com.google.auto.value:auto-value:$autoValueVer" annotationProcessor "com.google.auto.value:auto-value:$autoValueVer" (from T11-Model/ToDo/app/build.gradle)

We use autoValueVer via string interpolation, so it gets added to the artifact group and ID, to identify the specific version that we want. The annotationProcessor directive defines an annotation processor; compileOnly says “make this library available at compile time, but do not package its contents into the APK”. This gives us an overall set of dependencies that looks like: def supportVer="27.1.1" def autoValueVer="1.5.1" dependencies { implementation "com.android.support:recyclerview-v7:$supportVer" implementation "com.android.support:support-fragment:$supportVer" implementation 'com.android.support.constraint:constraint-layout:1.1.0' compileOnly "com.google.auto.value:auto-value:$autoValueVer" annotationProcessor "com.google.auto.value:auto-value:$autoValueVer" testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }

(from T11-Model/ToDo/app/build.gradle)

As before, Android Studio should now be requesting that you “Sync Now” — go ahead and click that link to allow the IDE to synchronize its build files.

Step #3: Adding the Annotation Next, just above the public abstract class ToDoModel line, add: @AutoValue

This will trigger Android Studio to ask you to add the import for com.google.auto.value.AutoValue, leaving us with: package com.commonsware.todo; import com.google.auto.value.AutoValue com.google.auto.value.AutoValue; @AutoValue public abstract class ToDoModel { }

131


DEFINING A MODEL This annotation tells the AutoValue annotation processor that we want it to codegenerate for us an immutable class that extends our abstract ToDoModel class. We will wind up using that subclass, though we will not refer to it directly very often, working instead with the API that we will define on ToDoModel itself.

Step #4: Defining a Builder If ToDoModel is immutable, clearly we need some way of creating instances of it, ones that contain the data we want in the model. After all, we cannot modify the ToDoModel after creating it, so we have to populate it up front. There are two options for doing this with AutoValue: 1. Defining a constructor that takes all the desired values, or 2. Defining a “builder” class that offers an API to build up an instance The “builder” pattern is used a lot in modern Java development, particularly on Android, so we will use that here. Inside the ToDoModel class, define a public static abstract class named Builder, and give it the @AutoValue.Builder annotation: package com.commonsware.todo; import com.google.auto.value.AutoValue com.google.auto.value.AutoValue; @AutoValue public abstract class ToDoModel { @AutoValue.Builder public static abstract class Builder { } }

This tells AutoValue that it should not only code-generate a subclass of ToDoModel that is immutable, but it also should code-generate a subclass of Builder to build instances of our ToDoModel. Inside the Builder class, add a single abstract method, named build() that returns a ToDoModel: @AutoValue.Builder public abstract static class Builder {

132


DEFINING A MODEL public abstract ToDoModel build(); }

The code-generated subclass of Builder will implement this method to return an instance of the code-generated subclass of ToDoModel, though we will use it solely via the ToDoModel API. Then, add a static method named builder() to ToDoModel itself, to return instances of this code-generated Builder subclass: static Builder builder() { return new AutoValue_ToDoModel.Builder(); } (from T11-Model/ToDo/app/src/main/java/com/commonsware/todo/ToDoModel.java)

We refer to the Builder class inside ToDoModel as ToDoModel.Builder. AutoValue’s generated subclass simply has AutoValue_ appended to the front. Initially, that class name may show up in red:

Figure 105: Android Studio Error That is because we have not built the app after having made this change, and this new Java class does not yet exist. If you choose “Build” > “Make Module ‘app’” from the Android Studio main menu, the AutoValue annotation processor will get a chance to create this class, after which the error should go away. 133


DEFINING A MODEL

Step #5: Adding Properties There is one big problem with our ToDoModel: it has no data. In an ordinary “plain old Java object” (POJO), we would just add fields for the different pieces of data that we want our ToDoModel to track. With AutoValue, instead we add abstract getter methods for those pieces of data, where AutoValue will generate the implementation for us. And, on our Builder, we create abstract setter methods for those pieces of data, where the setter methods return the Builder itself, so calls can be chained. Once again, AutoValue will generate the actual implementation of those abstract methods for us. With that in mind, let’s add 5 properties to ToDoModel: • • • • •

A unique ID A flag to indicate if the task is completed or not A description, which will appear in the list Some notes, in case there is more information The date/time that the model was created on

To that end, add these five methods to ToDoModel, which serve as our getters: public abstract String id(); public abstract boolean isCompleted(); public abstract String description(); @Nullable public abstract String notes(); public abstract Calendar createdOn(); (from T11-Model/ToDo/app/src/main/java/com/commonsware/todo/ToDoModel.java)

This will require imports for android.support.annotation.Nullable and java.util.Calendar. The @Nullable annotation on notes() indicates that this value can be null; otherwise, AutoValue would demand that new ToDoModel instances be given notes as well as the other properties. Also, add these five builder-style setter methods to Builder: abstract Builder id(String id); public abstract Builder isCompleted(boolean isCompleted); public abstract Builder description(String desc); public abstract Builder notes(String notes); abstract Builder createdOn(Calendar date);

134


DEFINING A MODEL (from T11-Model/ToDo/app/src/main/java/com/commonsware/todo/ToDoModel.java)

Now, when we use a Builder, we can supply values for all of those properties.

Step #6: Populating Some Default Properties You will notice that the id() and createdOn() methods on the Builder are not public, but instead are “package-private”, available only to other classes in this Java package. At the moment, that is “a distinction without a difference”, as all of our classes are in the same com.commonsware.todo package. The id() and createdOn() properties are distinctive, though, in that we do not want new values for those to be created on a whim. Instead, we want to: • Use new values when creating a logically new instance of a to-do item, and • Later, use values loaded from a database, as we load in existing to-do items To simplify this a bit, we can offer a creator() method, separate from the builder() method, that fills in new ID and created-on values. We would use creator() when we are creating a new-to-the-universe to-do list item, and use builder() for other cases. Add this method to ToDoModel: public static Builder creator() { return builder() .isCompleted(false false) .id(UUID.randomUUID().toString()) .createdOn(Calendar.getInstance()); } (from T11-Model/ToDo/app/src/main/java/com/commonsware/todo/ToDoModel.java)

This will require an import for java.util.UUID. Here, we: • Call builder() to get a Builder • Assign a randomly-generated unique ID as the id() value, using Java’s UUID class • Use the current date and time for createdOn() • Indicate that this task is not yet completed 135


DEFINING A MODEL • Return the Builder, which we can use for setting things like the description and notes There is nothing stopping code from using the Builder and overriding our id() and createdOn() values. Here, we are simply trying to make it easier for developers using this code to “do the right thing” and follow our intended pattern for working with these model objects.

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • app/build.gradle • app/src/main/java/com/commonsware/todo/ToDoModel.java

136


Setting Up a Repository

So, now we have a ToDoModel. Wonderful! But, this raises the question: where do ToDoModel instances come from? In the long term, we will be storing our to-do items in a database. For the moment, to get our UI going, we can just cache them in memory. We could, if desired, have a server somewhere that is the “system of record” for our to-do items, with the local database serving as a persistent cache. Ideally, our UI code does not have to care about any of that. And, ideally, our code that does have to deal with all of the storage work does not care about how our UI is written. One pattern for enforcing that sort of separation is to use a repository. The repository handles all of the data storage and retrieval work. Exactly how it does that is up to the repository itself. It offers a fairly generic API that does not “get into the weeds” of the particular storage techniques that it uses. The UI layer works with the repository to get data, create new data, update or delete existing data, and so on, and the repository does the actual work. So, in this tutorial, we will set up a simple repository. Right now, that will just be an in-memory cache, but in later tutorials we will move that data to a database. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial. NOTE: Starting with this tutorial, we will stop reminding you about adding import statements for newly-referenced classes. By now, you should be used to the pattern 137


SETTING UP A REPOSITORY of adding import statements. If you see a class name show up in red, and Android Studio says that it does not know about that class, most likely you need to add an import statement for it.

Read Me! This tutorial assumes that you have learned about the repository pattern from somewhere. That could be: • From the official documentation, such as their material on app architecture • Android’s Architecture Components and its “The Repository Pattern” chapter • Other educational resources

Step #1: Adding the Class Once again, we need a new Java class. To add our repository class, right-click over the com.commonsware.todo package in the project tree in Android Studio, and choose “New” > “Java Class” from the context menu. As before, this brings up a dialog where we can define a new Java class, by default into the same Java package that we right-clicked over. Fill in ToDoRepository in the “Name” field . Leave the rest of the dialog alone, and click “OK” to create this class. ToDoRepository should show up in an editor, with an implementation like this: package com.commonsware.todo; public class ToDoRepository { }

Step #2: Offering a Singleton Typically, a repository is set up as a singleton: a global instance of the repository class that everything routes to. We have to be careful, as singletons are intentional memory leaks, and we do not want to accidentally run out of memory because of this singleton. In our case, each ToDoModel object is fairly small, unless the user types in a lot of notes, and so we should be safe for any reasonable use of this app. Creating singletons gets a bit tricky, due to Java threads. Eventually, we will start getting into using threads in these tutorials. We do not want a “race condition”, where two threads both try to set up the singleton at the same time. So we will need to take that into account when arranging to make the singleton available to others. 138


SETTING UP A REPOSITORY One way to avoid the race condition is to initialize our singleton instance right away when the ToDoRepository class loads. With that in mind, modify ToDoRepository to look like this: package com.commonsware.todo; public class ToDoRepository { private static volatile ToDoRepository INSTANCE=new new ToDoRepository(); public synchronized static ToDoRepository get() { return INSTANCE; } }

The singleton itself is the INSTANCE field. This field is static, so there will only be one INSTANCE. The volatile keyword helps teach the Java compiler and the Android runtime that there may be multiple threads accessing this field at the same time. To allow outside access to the singleton, we have a static method named get() to return it, so the INSTANCE field can be private. The get() method has the synchronized keyword, which indicates that only one thread at a time can call this method. While get() is being executed on one thread, another thread that tries to call get() will block until the first get() call completes. At the present time, that synchronized keyword is not necessary, but we will want it later on, when our get() method becomes more complicated in future tutorials.

Step #3: Creating Some Fake Data At the moment, our repository has no data. We need to fix this, so that we have some starter to-do items to show in our UI. But, right now, our UI can only show to-do items; we have not built any forms to allow the user to create new to-do items. So, for the time being, we can have our repository create some fake data, which we can then replace with user-supplied data later on. To the list of fields in ToDoRepository, add: private List<ToDoModel> items=new new ArrayList<>(); (from T12-Repository/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

This is just a simple ArrayList to hold a bunch of ToDoModel objects.

139


SETTING UP A REPOSITORY Then, add a private constructor, where we can set up some fake data: private ToDoRepository() { items.add(ToDoModel.builder() .description("Buy a copy of _Exploring Android_") .notes("See https://wares.commonsware.com") .isCompleted(true true) .build()); items.add(ToDoModel.builder() .description("Complete all of the tutorials") .build()); items.add(ToDoModel.builder() .description("Write an app for somebody in my community") .notes("Talk to some people at non-profit organizations to see what they need!") .build()); }

(from T12-Repository/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

Here, we get to use ToDoModel and its Builder for the first time. We set up three todo items, each with a description(). Two of them have notes. The first one is marked as completed; the other two will not yet be completed. All get added to the ArrayList and so are held onto by the repository.

Step #4: Publishing Our Data Finally, we need a way for code outside of the repository to get access to the list of to-do items. To that end, add this all() method to ToDoRepository: public List<ToDoModel> all() { return new ArrayList<>(items); } (from T12-Repository/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

In principle, we could just return the items ArrayList. However, a key principle behind a repository is that only the repository can change its data. After all, in the long term, we may have databases or servers to address. While the ToDoModel objects are immutable, the ArrayList itself is not. Whoever holds the items ArrayList can add and remove items from it… and we want to limit that to be the job of the ToDoRepository. So, we return a copy of the ArrayList, so that callers have access to all of the to-do items, and could even add or remove items from their copy, but that would not reflect in the items ArrayList. Here, items is the “system of record”, the official storage point for the to-do items, and only the ToDoRepository can modify items. At this point, your ToDoRepository class should resemble: 140


SETTING UP A REPOSITORY package com.commonsware.todo; import java.util.ArrayList java.util.ArrayList; import java.util.List java.util.List; public class ToDoRepository { private static volatile ToDoRepository INSTANCE=new new ToDoRepository(); private List<ToDoModel> items=new new ArrayList<>(); public synchronized static ToDoRepository get() { return INSTANCE; } private ToDoRepository() { items.add(ToDoModel.builder() .description("Buy a copy of _Exploring Android_") .notes("See https://wares.commonsware.com") .isCompleted(true true) .build()); items.add(ToDoModel.builder() .description("Complete all of the tutorials") .build()); items.add(ToDoModel.builder() .description("Write an app for somebody in my community") .notes("Talk to some people at non-profit organizations to see what they need!") .build()); } public List<ToDoModel> all() { return new ArrayList<>(items); } }

(from T12-Repository/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

What We Changed The bookâ&#x20AC;&#x2122;s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: â&#x20AC;˘ app/src/main/java/com/commonsware/todo/ToDoRepository.java

141


Testing Our Repository

Right now, we have no idea if our ToDoRepository works. Probably it does. After all, there is not much to the code. Besides, this is a book, and books never have mistakes, right? (right?!?) In the real world, though, you do not have a set of tutorials for every bit of code that you want to write. Along the way, writing tests will help you confirm that the code that you wrote actually works. So, in this tutorial, we will start adding some tests to our project. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Read Me! This tutorial assumes that you have learned about JUnit 4 and instrumentation tests from somewhere. That could be: • From the official documentation, such as their material on app testing • The Busy Coder’s Guide to Android Development and its “Testing with JUnit4” chapter • Other educational resources

143


TESTING OUR REPOSITORY

Step #1: Examine Our Existing Tests The good news is that the project you imported to start these tutorials already has some tests written for you. (no, this does not mean that you are done with testing) Tests in Android modules go into “source sets” that are peers of main/. If you examine your project in Android Studio, you will see that there are three directories in app/src/: androidTest/, main/, and test/:

Figure 106: Android Studio, Showing Source Sets androidTest/

holds “instrumentation tests”. Simply put, these are tests of our code that run on an Android device or emulator, just as our app does. If you go into that directory, you will see that it has its own java/ tree, with an ExampleInstrumentedTest defined there: package com.commonsware.todo; import android.content.Context android.content.Context; import android.support.test.InstrumentationRegistry android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4 android.support.test.runner.AndroidJUnit4; import org.junit.Test org.junit.Test; import org.junit.runner.RunWith org.junit.runner.RunWith; import static org.junit.Assert.*;

/** * Instrumented test, which will execute on an Android device.

144


TESTING OUR REPOSITORY * * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test public void useAppContext() throws Exception { // Context of the app under test. Context appContext=InstrumentationRegistry.getTargetContext(); assertEquals("com.commonsware.todo", appContext.getPackageName()); } }

test/

holds â&#x20AC;&#x153;unit testsâ&#x20AC;?. These are tests of our code that run in plain Java on our development machine. On the plus side, they run much faster, as we do not have to copy the test code over to a device or emulator, and a device or emulator is going to be slower than our development machine (usually). On the other hand, our development machine is not running Android, so we cannot easily test code that touches Android-specific classes and methods. Like androidTest/, test/ has its own java/ tree, with an ExampleUnitTest defined there: package com.commonsware.todo; import org.junit.Test org.junit.Test; import static org.junit.Assert.*;

/** * Example local unit test, which will execute on the development machine * (host). * * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> */ public class ExampleUnitTest { @Test public void addition_isCorrect() throws Exception { assertEquals(4, 2+2); } }

Neither of these test very much, let alone anything related to our own code.

145


TESTING OUR REPOSITORY

Step #2: Decide on Instrumentation Tests vs. Unit Tests So, which should we use? Instrumentation tests? Unit tests? Both? Inevitably, you wind up needing some instrumentation tests, as inevitably you will want to test something that is tied to Android. Anything that can be run as a unit test can be run as an instrumentation test. Hence, a simple approach is to use instrumentation tests for most things. Unit tests are great for: • Large projects, with thousands of tests, where the performance gain of unit tests is worthwhile • Testing plain Java code that might be used in places other than Android apps (e.g., on a server) So, we will leave ExampleUnitTest alone for the time being and turn our attention to ExampleInstrumentedTest.

Step #3: Rename the Test Case The existing instrumentation test case — ExampleInstrumentedTest — has two fundamental problems. One of these is the name, which is fine for a project from a template but makes little sense in our context. So, right-click over ExampleInstrumentedTest in the project tree and choose “Refactor” > “Rename” from the context menu. That will bring up a dialog where you can change the name of this class:

Figure 107: Android Studio Class Rename Dialog

146


TESTING OUR REPOSITORY Change the name in the field to RepoTests, then click the “Refactor” button. This will change both the name of the Java file and the Java class name within the file.

Step #4: Test the Repository A general pattern for test classes like RepoTests is: • Do some initial setup, for things that all of our test methods will want to use • Have 1+ test methods — ones with the @Test annotation — to exercise whatever it is that we are testing • If needed, clean up the stuff that we set up originally Android’s instrumentation test system uses JUnit, and at the present time, it specifically uses JUnit 4. JUnit 4 uses @Before and @After annotations to indicate methods that should be run before and after each of the test methods. With that in mind, replace RepoTests with: package com.commonsware.todo; import android.content.Context android.content.Context; import android.support.test.InstrumentationRegistry android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4 android.support.test.runner.AndroidJUnit4; import org.junit.Before org.junit.Before; import org.junit.Test org.junit.Test; import org.junit.runner.RunWith org.junit.runner.RunWith; import static org.junit.Assert.*; @RunWith(AndroidJUnit4.class) public class RepoTests { private ToDoRepository repo; @Before public void setUp() { repo=ToDoRepository.get(); } @Test public void getAll() { assertEquals(3, repo.all().size()); } }

147


TESTING OUR REPOSITORY (from T13-Tests/ToDo/app/src/androidTest/java/com/commonsware/todo/RepoTests.java)

Here, we: • Define a field to hold our repository • Obtain our repository in setUp(), which will be run @Before each of our tests methods • Test that the number of to-do items in the repository is 3 This is not a particularly good test. Eventually, we will stop hard-coding the to-do items, at which point there may be something other than 3 to-do items. But, for now, this test will suffice for getting our tests up and running.

Step #5: Run the Test Case In the “gutter” area of the editor, you should see a double-play icon on the line where the RepoTests class is declared, and a regular play icon on the line where the getAll() @Test method is declared:

Figure 108: Android Studio, Showing Play Icons The double-play icon will allow you to run all of the test methods defined in RepoTests, while the regular play icon will allow you to run an individual test method.

148


TESTING OUR REPOSITORY So, click on the double-play icon and choose “Run RepoTests” from the pop-up menu. That will bring up the same sort of dialog that we have seen before, asking where we want to run our tests — on an existing connected device, on an existing running emulator, or on some emulator that needs to be started. Choose whatever you have been using so far, and click “OK”. This will run our test… which fails:

Figure 109: Android Studio, Showing Failed Test

Step #6: Fix the Repository So, what went wrong? If you look at the stack trace in our test results, you will find this towards the bottom: Caused by: java.lang.IllegalStateException: Missing required properties: id createdOn at com.commonsware.todo.AutoValue_ToDoModel$Builder.build(AutoValue_ToDoModel.java:157) at com.commonsware.todo.ToDoRepository.[init](ToDoRepository.java:19) at com.commonsware.todo.ToDoRepository.[clinit](ToDoRepository.java:7) ... 29 more

Unless we take steps to the contrary, AutoValue wants us to provide values for all properties of our immutable models, and we are not doing that.

149


TESTING OUR REPOSITORY That is because of a mistake in creating our initial fake data. We have: private ToDoRepository() { items.add(ToDoModel.builder() .description("Buy a copy of _Exploring Android_") .notes("See https://wares.commonsware.com") .isCompleted(true true) .build()); items.add(ToDoModel.builder() .description("Complete all of the tutorials") .build()); items.add(ToDoModel.builder() .description("Write an app for somebody in my community") .notes("Talk to some people at non-profit organizations to see what they need!") .build()); }

This uses builder(), which does indeed give us a Builder that we can use for creating ToDoModel instances. However, we are not calling id() on the Builder, and for two of the three we are not calling isCompleted(). That is because we are using builder(), not creator(), which fills in those values for us: static Builder builder() { return new AutoValue_ToDoModel.Builder(); } public static Builder creator() { return builder() .isCompleted(false false) .id(UUID.randomUUID().toString()) .createdOn(Calendar.getInstance()); } (from T13-Tests/ToDo/app/src/main/java/com/commonsware/todo/ToDoModel.java)

So, revise the ToDoRepository constructor to use creator() instead of builder(): private ToDoRepository() { items.add(ToDoModel.creator() .description("Buy a copy of _Exploring Android_") .notes("See https://wares.commonsware.com") .isCompleted(true true) .build()); items.add(ToDoModel.creator() .description("Complete all of the tutorials") .build());

150


TESTING OUR REPOSITORY items.add(ToDoModel.creator() .description("Write an app for somebody in my community") .notes("Talk to some people at non-profit organizations to see what they need!") .build()); }

(from T13-Tests/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

You now have yet another way to run the tests: via the Android Studio main toolbar. You will see that the drop-down to the side of the “run” toolbar button has changed. It no longer shows “app”, but instead shows “RepoTests”:

Figure 110: Android Studio Toolbar, Showing “RepoTests” If you set the drop-down to show “app”, it will run the app; if you set the drop-down to show “RepoTests”, it will run the tests in the RepoTests class. Run the RepoTests tests, and you will see that it now succeeds:

Figure 111: Android Studio, Showing Successful Test

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • app/src/androidTest/java/com/commonsware/todo/RepoTests.java • app/src/main/java/com/commonsware/todo/ToDoRepository.java

151


Populating Our RecyclerView

We now have a repository with some fake to-do items. It would be helpful if the user could see these items in our MainActivity and its RosterListFragment. We have a RecyclerView in that fragment, and now we need to tie the data from the repository into the RecyclerView. Right now, we are going to take a fairly simplistic approach to the problem, having the fragment work directly with the repository. That will work for now, but it is not a great choice. Once we start allowing the user to view and edit to-do items, plus start saving this data in a database, we will need a more sophisticated approach. But, that is a task for the future — today, we will keep it simple. However, we will explore another feature of the Android ecosystem: the data binding framework. This makes it a bit easier to pour data from objects, such as our ToDoModel objects, into UI layouts. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Read Me! This tutorial assumes that you have learned about RecyclerView and data binding from somewhere. That could be: • From the official documentation, such as their overview of RecyclerView and data binding • The Busy Coder’s Guide to Android Development and its “RecyclerView” chapter and its “The Data Binding Framework” chapter 153


POPULATING OUR RECYCLERVIEW • Other educational resources

Step #1: Adding Data Binding Support In theory, adding data binding support is easy. In practice… it is easy, but not quite as easy as it could be. First, open app/build.gradle and add this closure to the android closure: dataBinding { enabled=true true } (from T14-RecyclerView/ToDo/app/build.gradle)

This will give you an android closure like: android { compileSdkVersion 27 defaultConfig { applicationId "com.commonsware.todo" minSdkVersion 21 targetSdkVersion 27 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } dataBinding { enabled=true true } }

(from T14-RecyclerView/ToDo/app/build.gradle)

Step #2: Defining a Row Layout Next, we need to define a layout resource to use for the rows in our roster of to-do items.

154


POPULATING OUR RECYCLERVIEW Right-click over the res/layout/ directory and choose “New” > “Layout resource file” from the context menu. In the dialog that appears, fill in todo_row as the “File name” and ensure that the “Root element” is set to android.support.constraint.ConstraintLayout (which should be the default value). Then, click “OK” to close the dialog and create the mostly-empty resource file. For now, each row will be a CheckBox. This can show the description of the to-do item along with the is-completed status (checked items being those that are completed). So, drag a CheckBox from the “Buttons” category in the Palette into the preview area:

Figure 112: Android Studio Layout Designer, Showing CheckBox Widget Use the round grab handles to drag connections from the CheckBox to the four sides of the ConstraintLayout:

155


POPULATING OUR RECYCLERVIEW

Figure 113: Android Studio Layout Designer, Showing Centered CheckBox In the “Attributes” tool, change the layout_width drop-down to be match_constraint (a.k.a., 0dp). This will have the CheckBox stretch to fill all available space on the horizontal axis:

156


POPULATING OUR RECYCLERVIEW

Figure 114: Android Studio Layout Designer, Showing Stretched CheckBox Back in the “Attributes” tool, scroll to the bottom and click the “View all attributes” link. This switches the tool to a long list of attributes that are available on a CheckBox:

157


POPULATING OUR RECYCLERVIEW

Figure 115: Android Studio Layout Designer, Showing All CheckBox Attributes Make the following changes: Attribute NameNew Value id

desc

ellipsize

end

maxLines

3

(for ellipsize, choose end in the drop-down list) The latter two attributes say that we will show up to three lines for the description, and if the description exceeds that amount of space, put an ellipsis (“…”) after whatever fits, at the end. Then, scroll to the textSize attribute. You should see a “…” button in that attribute, when you hover your mouse over the row. Click on that “…” button to bring up a resource selector dialog, this time for dimension resources:

158


POPULATING OUR RECYCLERVIEW

Figure 116: Android Studio Layout Designer, Resource Selector Dialog Click the “Add new resource” drop-down in the corner of the dialog, and in there click the “New dimen Value” option. That brings up a dialog to define a new dimension resource:

159


POPULATING OUR RECYCLERVIEW

Figure 117: Android Studio Layout Designer, New Dimension Resource Dialog For the “Resource name”, fill in desc_size. For the “Resource value”, fill in 16sp, for 16 “scaled pixels”. The actual size will not only vary based upon the screen density, but also on the user’s chosen text size in the the device settings. Click “OK” to close up both of those dialogs, and notice that now textSize is set to @dimen/desc_size. Finally, modify layout_height on the ConstraintLayout itself to be wrap_content. You can do this by clicking on the ConstraintLayout entry in the “Component Tree” view (below the Palette), then changing the layout_height in the “Attributes” pane. We will have some other changes to make later, but this will get us going for now. If you switch to the “Text” sub-tab, the XML of the layout should resemble: <?xml version="1.0" encoding="utf-8"?> <android.support.constraint.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="wrap_content"> > <CheckBox android:id="@+id/desc"

160


POPULATING OUR RECYCLERVIEW android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:ellipsize="end" android:minLines="3" android:text="CheckBox" android:textSize="@dimen/desc_size" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout>

Step #3: Adding a Stub ViewHolder RecyclerView relies upon custom subclasses of RecyclerView.Adapter and RecyclerView.ViewHolder to do “the heavy lifting” of populating its contents. The ViewHolder is responsible for a single item in the RecyclerView, such as a single row in a scrolling list. The Adapter is responsible for creating and populating the ViewHolder instances for each of our model objects, as needed.

So, let’s start by creating a stub subclass of RecyclerView.ViewHolder. Right-click over the com.commonsware.todo Java package and choose “New” > “Java Class” from the context menu. Fill in RosterRowHolder as the “Name”. Start typing in RecyclerView for the “Superclass”, then choose android.support.v7.widget.RecyclerView from the drop-down list. Then, continue typing, starting to fill in the .ViewHolder part, and choose android.support.v7.widget.RecyclerView.ViewHolder from the drop-down list. Check the “Show Select Overrides Dialog” checkbox. Leave everything else alone in the dialog, and click “OK” to create the class. Checking that checkbox will cause another dialog to appear, one where you can specify some methods that we want to override when creating this class:

161


POPULATING OUR RECYCLERVIEW

Figure 118: Android Studio, Select Methods to Override/Implement Dialog Highlight the constructor, which appears as ViewHolder(itemView:View) in that dialog. Then click “OK” to create the constructor and close the dialog. This will give you a do-nothing RosterRowHolder: package com.commonsware.todo; import android.support.v7.widget.RecyclerView android.support.v7.widget.RecyclerView; import android.view.View android.view.View; public class RosterRowHolder extends RecyclerView.ViewHolder { public RosterRowHolder(View itemView) { super super(itemView); } }

Step #4: Creating a Stub Adapter Next, do some of the same things, but for RecyclerView.Adapter, to create a RosterListAdapter: • Right-click over the com.commonsware.todo package • Choose “New” > Java Class" from the context menu • Fill in RosterListAdapter as the “Name” 162


POPULATING OUR RECYCLERVIEW • Choose android.support.v7.widget.RecyclerView.Adapter This time, though, leave the “Show Select Overrides Dialog” checkbox unchecked, before clicking “OK” to create the class. You will immediately get a red undersquiggle in Android Studio, complaining about missing methods on this class. There are a few methods that we are required to implement. However, first, we need to tie RosterListAdapter to RosterRowHolder. RecyclerView.Adapter uses Java generics. We are not merely creating RecyclerView.Adapter subclass — we are creating one that manages RosterRowHolder instances.

a

So, in the class declaration, replace: public class RosterListAdapter extends RecyclerView.Adapter

with: public class RosterListAdapter extends RecyclerView.Adapter<RosterRowHolder>

This refines our RosterListAdapter class and ties it to the RosterRowHolder. Now, we need to address the error. Put the text cursor somewhere in the red undersquiggle section. Then, press Alt-Enter (or Option-Return on macOS) to bring up the “quick fix” list:

Figure 119: Android Studio, Showing “Quick-Fix” Options Choose “Implement methods” from that drop-down list. This will pop up another edition of the “Select Methods to Implement” dialog that we got previously from 163


POPULATING OUR RECYCLERVIEW checking the “Show Select Overrides Dialog” checkbox in the new-class dialog. Three methods should be highlighted. Leave them highlighted, and click “OK” to generate those methods. The code that is generated for us not only fixes the compile error, but it also references RosterRowHolder in two of the methods: package com.commonsware.todo; import android.support.v7.widget.RecyclerView android.support.v7.widget.RecyclerView; import android.view.ViewGroup android.view.ViewGroup; public class RosterListAdapter extends RecyclerView.Adapter<RosterRowHolder> { @Override public RosterRowHolder onCreateViewHolder(ViewGroup parent, int viewType) { return null null; } @Override public void onBindViewHolder(RosterRowHolder holder, int position) { } @Override public int getItemCount() { return 0; } }

We could have checked the “Show Select Overrides Dialog” checkbox in the newclass dialog, then selected those three methods. However, at that point, Android Studio did not know what ViewHolder class we wanted to use with this Adapter. It would have used ViewHolder, not RosterRowHolder, in the onCreateViewHolder() and onBindViewHolder() methods, and we would have needed to fix those afterwards.

Step #5: Retrieving Our Model Data Our RosterListAdapter needs our roster of ToDoModel objects, to be able to display them in the RecyclerView. Right now, we can just have the adapter get that list from the ToDoRepository directly. That is not a great long-term solution, and we will be changing this in future tutorials, but it will work fine for the moment.

164


POPULATING OUR RECYCLERVIEW So, add a models field to RosterListAdapter to hold our ToDoModel objects: final private List<ToDoModel> models; (from T14-RecyclerView/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

Then, add a RosterListAdapter constructor that populates the models field by calling all() on our ToDoRepository singleton: RosterListAdapter(RosterListFragment host) { models=ToDoRepository.get().all(); }

Step #6: Adding the Data Binding We know that we want to show ToDoModel objects in our RecyclerView rows. That means that we can add data binding expressions to our layout resource to be able to pull data from the models into our CheckBox widget. Add the following to the XML file, after the <?xml version="1.0" encoding="utf-8"?> line (if you have one) and before the <android.support.constraint.ConstraintLayout> element: <layout> <data> <variable name="model" type="com.commonsware.todo.ToDoModel" /> </data>

You will also need to add </layout> to the end of the file. The <layout> element allows us to add some metadata to the layout resource, above and beyond the widgets that we have. The <data> element is where we configure metadata for data binding. The <variable> element is where we state that we want to bind data from a certain type of object (com.commonsware.todo.ToDoModel), and that we will refer to that object by the name model elsewhere in the resource. On the <CheckBox> element, replace the existing android:text attribute with: android:text="@{model.description()}"

165


POPULATING OUR RECYCLERVIEW (from T14-RecyclerView/ToDo/app/src/main/res/layout/todo_row.xml)

The @{} notation indicates that this is a data binding expression. Rather than it being some fixed value, we have a snippet of code that will be executed at runtime to fill in this attribute. Specifically, we call the description() element on our ToDoModel object to fill in the caption of the CheckBox. Then, add the following attribute to the CheckBox: android:checked="@{model.isCompleted()}" (from T14-RecyclerView/ToDo/app/src/main/res/layout/todo_row.xml)

The android:checked attribute on a CheckBox indicates whether or not the CheckBox is checked. Here, we populate it with another binding expression, calling isCompleted() on our ToDoModel. Hence, completed items will show up as checked; items not yet completed will show up as unchecked. At this point, the layout resource XML should look something like this: <?xml version="1.0" encoding="utf-8"?> <layout> <data> <variable name="model" type="com.commonsware.todo.ToDoModel" /> </data> <android.support.constraint.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="wrap_content"> > <CheckBox android:id="@+id/desc" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:ellipsize="end" android:maxLines="3" android:checked="@{model.isCompleted()}" android:text="@{model.description()}" android:textSize="@dimen/desc_size" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />

166


POPULATING OUR RECYCLERVIEW </android.support.constraint.ConstraintLayout> </layout>

(from T14-RecyclerView/ToDo/app/src/main/res/layout/todo_row.xml)

Step #7: Completing the Adapter Now, we can start filling in the implementations of those stub methods in our RosterListAdapter, plus get our RosterRowAdapter working. One of them is easy: getItemCount() on RosterListAdapter should return the size of the List as the number of items to be displayed, since we want to display all of them: @Override public int getItemCount() { return models.size(); } (from T14-RecyclerView/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

The job of onCreateViewHolder() is to create instances of a ViewHolder, including working with the ViewHolder to set up the widgets. Since our widgets are defined in a layout resource, we will need a LayoutInflater to accomplish this. The best way to get a LayoutInflater is to call getLayoutInflater() on an activity or fragmentâ&#x20AC;Ś but RosterListAdapter has none of these. So, add another field to RosterListAdapter, named host, whose type is RosterListFragment: final private RosterListFragment host; (from T14-RecyclerView/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

Then, add a RosterListFragment parameter to the RosterListAdapter constructor, and assign it to the host field: RosterListAdapter(RosterListFragment host) { models=ToDoRepository.get().all(); this this.host=host; } (from T14-RecyclerView/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

167


POPULATING OUR RECYCLERVIEW Part of the data binding framework is a code generator, which generates Java classes from our layout resources. In particular, since we created a todo_row layout resource, the code generator will generate a ToDoRowBinding class. This class does a few things: • It knows its associated layout resource and can work with a LayoutInflater to set up those widgets • It allows us to assign values for the variables declared in the <variable> elements in the layout • It provides easy access to named widgets within the layout from our Java code, should we need that (and we will, in future tutorials) With that in mind, modify onCreateViewHolder() in RosterListAdapter to be: @Override public RosterRowHolder onCreateViewHolder(ViewGroup parent, int viewType) { TodoRowBinding binding= TodoRowBinding.inflate(host.getLayoutInflater(), parent, false false); return new RosterRowHolder(binding); } (from T14-RecyclerView/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

Here, we use our host and getLayoutInflater(), plus the inflate() method on ToDoRowBinding, to set up a ToDoRowBinding object. The particular flavor of inflate() that we are calling says: • Use this LayoutInflater to inflate the layout resource (host.getLayoutInflater()) • The widgets in that layout resource eventually will be children of a certain parent (parent)… • …but do not add them as children right away (false) (some container classes, like RelativeLayout, really need to know their parent in order to work properly, so we use this standard recipe for calling inflate()) However, onCreateViewHolder() will have a compile error, as we are calling a constructor on RosterRowHolder that does not exist. We have a constructor on RosterRowHolder, but it takes a View, not a ToDoRowBinding. So, modify the RosterRowHolder constructor to look like this:

168


POPULATING OUR RECYCLERVIEW public RosterRowHolder(TodoRowBinding binding) { super super(binding.getRoot()); }

getRoot()

on a binding object returns the root widget of the inflated layout, which in our case is the ConstraintLayout. Now our RosterListAdapter knows to create RosterRowHolder objects as needed. However, somewhere, we need to get a ToDoModel object and supply that to the data binding code, to fill in the text and is-completed state for the CheckBox. With that in mind, modify onBindViewHolder() on RosterListAdapter to look like this: @Override public void onBindViewHolder(RosterRowHolder holder, int position) { holder.bind(models.get(position)); } (from T14-RecyclerView/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

onBindViewHolder()

is called when RecyclerView wants us to update a ViewHolder to reflect data from some item in the RecyclerView. We are given the position of that item, and since our items are coming from the models List, we can call get() on the List to retrieve the appropriate ToDoModel. This will have a compile error, as there is no bind() method on RosterRowHolder. The objective of bind() is to populate our widgets, and since we are using the data binding framework, that comes in the form of calling binding methods on the ToDoRowBinding object. However, right now, RosterRowHolder is getting its ToDoRowBinding object in its constructorâ&#x20AC;Ś and not holding onto it. So, add a field to RosterRowHolder named binding: final private TodoRowBinding binding; (from T14-RecyclerView/ToDo/app/src/main/java/com/commonsware/todo/RosterRowHolder.java)

Then, modify the RosterRowHolder constructor to save the binding that it receives as a parameter: public RosterRowHolder(TodoRowBinding binding) { super super(binding.getRoot());

169


POPULATING OUR RECYCLERVIEW this this.binding=binding; } (from T14-RecyclerView/ToDo/app/src/main/java/com/commonsware/todo/RosterRowHolder.java)

Now we can add an implementation of bind() to RosterRowHolder that uses binding: void bind(ToDoModel model) { binding.setModel(model); binding.executePendingBindings(); } (from T14-RecyclerView/ToDo/app/src/main/java/com/commonsware/todo/RosterRowHolder.java)

Here, we do two things: 1. Call setModel(), to tell the data binding framework to take this particular ToDoModel and update the widgets from that 2. Call executePendingBindings(), as we want the data binding framework to evaluate its binding expressions now, not sometime later

Step #8: Wiring Up the RecyclerView Now, we can teach RosterListFragment to use our RosterListAdapter. Right now, RosterListFragment is fairly basic: package com.commonsware.todo; import import import import import import import

android.os.Bundle android.os.Bundle; android.support.annotation.NonNull android.support.annotation.NonNull; android.support.annotation.Nullable android.support.annotation.Nullable; android.support.v4.app.Fragment android.support.v4.app.Fragment; android.view.LayoutInflater android.view.LayoutInflater; android.view.View android.view.View; android.view.ViewGroup android.view.ViewGroup;

public class RosterListFragment extends Fragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.todo_roster, container, false false);

170


POPULATING OUR RECYCLERVIEW } }

If you recall, our layout consisted of a TextView placeholder and a RecyclerView, inside of a ConstraintLayout: <?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout 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="com.commonsware.todo.MainActivity"> > <TextView android:id="@+id/empty" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/msg_empty" android:textAppearance="?android:attr/textAppearanceMedium" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <android.support.v7.widget.RecyclerView android:id="@+id/items" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout>

(from T14-RecyclerView/ToDo/app/src/main/res/layout/todo_roster.xml)

We will need access to both of those widgets. So, add fields for each of them to RosterListFragment: private RecyclerView rv; private View empty; (from T14-RecyclerView/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

Then, modify onCreateView() to fill in those fields as part of its work: @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

171


POPULATING OUR RECYCLERVIEW View result=inflater.inflate(R.layout.todo_roster, container, false false); rv=result.findViewById(R.id.items); empty=result.findViewById(R.id.empty); return result; } (from T14-RecyclerView/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

Finally, add an onViewCreated() method to RosterListFragment: @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { rv.setLayoutManager(new new LinearLayoutManager(getActivity())); DividerItemDecoration decoration=new new DividerItemDecoration(getActivity(), LinearLayoutManager.VERTICAL); rv.addItemDecoration(decoration); rv.setAdapter(new new RosterListAdapter(this this)); empty.setVisibility(View.GONE); } (from T14-RecyclerView/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

Here, we: • Tell the RecyclerView that it is to be in the form of a vertically-scrolling list, by calling setLayoutManager() and supplying a LinearLayoutManager • Add divider lines between the rows by creating a DividerItemDecoration and adding it as a decoration to the RecyclerView • Tell the RecyclerView to get its items from an instance of •

RosterListAdapter Hide the empty widget,

by setting its visibility to be GONE

Step #9: Seeing the Results You can now run the app. Note that you want to double-check the “run configurations” drop-down in the Android Studio toolbar, adjacent to the run button. Make sure that it says “app”, as we had been running some tests in the previous tutorial, so there will be more than one entry in that drop-down list now. The app will show your hard-coded to-do items in the list: 172


POPULATING OUR RECYCLERVIEW

Figure 120: ToDo App, When Launched, Showing Hard-Coded Tasks

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • • • • •

app/build.gradle app/src/main/res/layout/todo_row.xml app/src/main/java/com/commonsware/todo/RosterRowHolder.java app/src/main/java/com/commonsware/todo/RosterListAdapter.java app/src/main/java/com/commonsware/todo/RosterListFragment.java

173


Extending the Repository

Right now, our repository makes to-do items available… and that’s it. We need to be able to create, update, and delete to-do items as well. That’s the focus on this tutorial: extending the repository for the full set of CRUD operations (Create, Read, Update, and Delete). And, along the way, we will extend our tests of the repository to match. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Step #1: Adding and Testing add() Add this add() method to ToDoRepository: public void add(ToDoModel model) { items.add(model); } (from T15-CRUD/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

Right now, we do not need to do anything more elaborate than just appending the new model to the list. However, in terms of testing this (and the upcoming edit() and delete() methods), we have a problem. Right now, RepoTests uses the singleton instance of ToDoRepository: package com.commonsware.todo; import android.content.Context android.content.Context;

175


EXTENDING THE REPOSITORY import android.support.test.InstrumentationRegistry android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4 android.support.test.runner.AndroidJUnit4; import org.junit.Before org.junit.Before; import org.junit.Test org.junit.Test; import org.junit.runner.RunWith org.junit.runner.RunWith; import static org.junit.Assert.*; @RunWith(AndroidJUnit4.class) public class RepoTests { private ToDoRepository repo; @Before public void setUp() { repo=ToDoRepository.get(); } @Test public void getAll() throws Exception { assertEquals(3, repo.all().size()); } }

That is fine for read-only operations, but once we start adding, editing, and deleting models, we are changing what is in the repository, and that may affect future tests using that repository. Ideally, we would use a fresh repository for each test method. To make this work, remove the private keyword from the ToDoRepository constructor, so it is package-private: ToDoRepository() { items.add(ToDoModel.creator() .description("Buy a copy of _Exploring Android_") .notes("See https://wares.commonsware.com") .isCompleted(true true) .build()); items.add(ToDoModel.creator() .description("Complete all of the tutorials") .build()); items.add(ToDoModel.creator() .description("Write an app for somebody in my community") .notes("Talk to some people at non-profit organizations to see what they need!") .build()); }

(from T15-CRUD/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

176


EXTENDING THE REPOSITORY Our instrumentation tests happen to be in the same Java package as the production code. This means that we are doing white-box testing: we can access package-private members, such as constructors. Modify setUp() in RepoTests to create a new ToDoRepository: @Before public void setUp() { repo=new new ToDoRepository(); } (from T15-CRUD/ToDo/app/src/androidTest/java/com/commonsware/todo/RepoTests.java)

Then, add an add() test method to RepoTests: @Test public void add() { ToDoModel model=ToDoModel.creator() .isCompleted(true true) .description("foo") .build(); repo.add(model); List<ToDoModel> models=repo.all(); assertEquals(4, models.size()); assertThat(models, hasItem(model)); } (from T15-CRUD/ToDo/app/src/androidTest/java/com/commonsware/todo/RepoTests.java)

Here, we: • Create a new ToDoModel via the Builder returned by creator() • add() it to the repository • Assert that we now have four items in the repository and this specific new model is one of those four The hasItem() method will show up in red, as that is a static method that we can import. If you use Alt-Enter ( Option-Return on macOS) for the import quickfix, you will get a few candidate implementations, all from the hamcrest test library. Choose the org.hamcrest.Matchers.hasItem edition of the method to import.

177


EXTENDING THE REPOSITORY If you run RepoTests again from the Android Studio toolbar — or by clicking on the double-play icon in the gutter next to the class declaration — you should see that this test succeeds.

Step #2: Adding and Testing replace() Since a ToDoModel instance is immutable, we need a way to replace an existing ToDoModel instance with an updated one. Those will have the same id() value, but the description(), notes(), and isCompleted() values could differ. Right now, we just have a bunch of models in a list. Eventually, we will need to start thinking about what order they should be in, such as sorting them alphabetically. But that is a UI concern, by and large. For the moment, let’s update the repository’s list of models by replacing the entry in the list, so if there are ten to-do items and the user replaces the sixth one, the replacement will be the sixth one as well. To that end, add the following replace() implementation to ToDoRepository: public void replace(ToDoModel model) { for (int i=0;i<items.size();i++) { if (model.id().equals(items.get(i).id())) { items.set(i, model); } } } (from T15-CRUD/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

We are using an ArrayList for the list of models. Hence, to find a model by its ID, we need to iterate over that list. We could use some other collection type, such as a HashMap, that makes replacement easier, but it would make other things (e.g., a stable order of items) harder. Since all of this code will be replaced by a database in later tutorials, though, switching to a HashMap may not be worthwhile, and so we will stick with ArrayList for now. To create a replacement ToDoModel, it would be helpful if we could get a Builder that is pre-populated with the existing values for the description, notes, and so forth. One pattern for this is to have a toBuilder() method that converts an existing object into a Builder to create an identical object. Then, we can just call methods on the Builder for the individual things that we want to change, such as the description.

178


EXTENDING THE REPOSITORY With that in mind, add this toBuilder() method to ToDoModel: public Builder toBuilder() { return builder() .id(id()) .isCompleted(isCompleted()) .description(description()) .notes(notes()) .createdOn(createdOn()); } (from T15-CRUD/ToDo/app/src/main/java/com/commonsware/todo/ToDoModel.java)

This takes a plain Builder and calls the builder method for each of the five properties. Then, add this test method to RepoTests: @Test public void replace() { ToDoModel original=repo.all().get(1); ToDoModel edited=original.toBuilder() .isCompleted(true true) .description("Currently on Tutorial #15") .build(); repo.replace(edited); assertEquals(3, repo.all().size()); assertSame(edited, repo.all().get(1)); } (from T15-CRUD/ToDo/app/src/androidTest/java/com/commonsware/todo/RepoTests.java)

Here, we: • • • •

Grab the second item out of the list of to-do items Create a modified edition of that item replace() the original item with the modified item Assert that we still have three model objects in the repo and that the second item in the revised list is the modified item

As with hasItem() from before, assertSame() is a static method that we can import. Choose org.junit.Assert.assertSame from the list of candidates. If you run RepoTests again, you should see that this test succeeds. 179


EXTENDING THE REPOSITORY

Step #3: Adding and Testing delete() Finally, we need a delete() method on ToDoRepository. The simple approach would be to treat it like add() and simply call remove() on the ArrayList. However, if delete() is passed in a modified ToDoModel — one with the correct ID but not the actual instance in the list — the remove() call will fail. It is safer to find the model with the proper ID and remove that one. So, add the following delete() method to ToDoRepository: public void delete(ToDoModel model) { for (ToDoModel original : items) { if (model.id().equals(original.id())) { items.remove(original); return return; } } } (from T15-CRUD/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

We once again iterate over the list, though this time we use the simpler Java for-each syntax, since we do not need the index of the match. When we find a match, we remove the object from the list and return. Then, add the corresponding delete() method to RepoTests: @Test public void delete() { assertEquals(3, repo.all().size()); repo.delete(repo.all().get(0)); assertEquals(2, repo.all().size()); repo.delete(repo.all().get(0).toBuilder().build()); assertEquals(1, repo.all().size()); } (from T15-CRUD/ToDo/app/src/androidTest/java/com/commonsware/todo/RepoTests.java)

Here, we: • • • •

Confirm that we have three items Delete the first one Confirm that we have two items Create an identical instance of the first one and delete that one 180


EXTENDING THE REPOSITORY • Confirm that we have one item And, once again, if you run RepoTests again, you should see that this test succeeds.

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • app/src/main/java/com/commonsware/todo/ToDoRepository.java • app/src/androidTest/java/com/commonsware/todo/RepoTests.java • app/src/main/java/com/commonsware/todo/ToDoModel.java

181


Tracking the Completion Status

We have checkboxes in the list to show the completion status. However, the user can toggle these checkboxes. Right now, that is only affecting the UI – our models still have the old data. We should find out when the user toggles the checked state of a checkbox, then update the associated model to match. So, that’s what we will work on in this tutorial. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Step #1: Injecting Our RosterRowHolder Somewhere along the line, we need to register an OnCheckedChangedListener on the CheckBox in each row. Since we are using the data binding framework, we can let it do that registration, passing control to us when the user checks (or unchecks) the CheckBox. In theory, we could have a data binding expression in the layout that directly updates the model. After all, the ToDoModel is what we are binding into the layout, via the model variable. In fact, the data binding framework has support for this, through what is known as two-way data binding. However, ToDoModel is immutable, so two-way data binding is not an option for us. Instead, we can add another variable: our RosterRowHolder. To do that, add another <variable> element to res/layout/todo_row.xml:

183


TRACKING THE COMPLETION STATUS <variable name="holder" type="com.commonsware.todo.RosterRowHolder" /> (from T16-Completion/ToDo/app/src/main/res/layout/todo_row.xml)

Then, modify bind() on RosterRowHolder to call setHolder() on the TodoRowBinding: void bind(ToDoModel model) { binding.setModel(model); binding.setHolder(this this); binding.executePendingBindings(); } (from T16-Completion/ToDo/app/src/main/java/com/commonsware/todo/RosterRowHolder.java)

setHolder()

will show up in red until Android Studio gets around to realizing that the data binding framework generated the method. This may require you to close and reopen the project. Now we can start using the holder variable as needed.

Step #2: Binding to the Checked Event The binding expressions that we already have in the layout â&#x20AC;&#x201D; on android:checked and android:text â&#x20AC;&#x201D; are setting standard properties of the UI. We can also add binding expressions for some event handlers, routing them to our Java code. With that in mind, add the following attribute to the CheckBox in the res/layout/ todo_row.xml resource: android:onCheckedChanged="@{(cb, isChecked) -> holder.completeChanged(model, isChecked)}"

(from T16-Completion/ToDo/app/src/main/res/layout/todo_row.xml)

Here, we have a lambda expression, where we are calling a completeChanged() method on the RosterRowHolder, passing in the ToDoModel bound to this row plus the new status of the CheckBox (isChecked). The entire res/layout/todo_row.xml resource should now resemble: <?xml version="1.0" encoding="utf-8"?> <layout>

184


TRACKING THE COMPLETION STATUS <data> <variable name="model" type="com.commonsware.todo.ToDoModel" /> <variable name="holder" type="com.commonsware.todo.RosterRowHolder" /> </data> <android.support.constraint.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="wrap_content"> > <CheckBox android:id="@+id/desc" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:checked="@{model.isCompleted()}" android:ellipsize="end" android:maxLines="3" android:onCheckedChanged="@{(cb, isChecked) -> holder.completeChanged(model, isChecked)}" android:text="@{model.description()}" android:textSize="@dimen/desc_size" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>

(from T16-Completion/ToDo/app/src/main/res/layout/todo_row.xml)

And, unfortunately, from this point forward, you will no longer be able to use the graphical designer to manipulate this layout in its current state, due to a bug in Android Studio 3.1. The designer does not like this binding expression, even though it is valid. To use the graphical designer for this layout, we will have to remove this attribute, make our changes, and then put the attribute back when we are done. Hopefully, this bug will get fixed reasonably soon. And, fortunately, we only need to make one more set of changes to this layout, in the next tutorial.

Step #3: Passing the Event Up the Chain Now we know our model and its new CheckBox state. We need to modify our repository to reflect the change in the model state.

185


TRACKING THE COMPLETION STATUS We could have the RosterRowHolder do that. After all, right now, we are working directly with the repository in RosterListAdapter, and there is little that is stopping us from doing the same thing in RosterRowHolder. However, it is best to minimize the number of places that you are modifying your data. The more your modelmanipulating code is scattered, the more difficult it will be to change that code, such as when we want to start storing this stuff in a database. Since we already are working with the repository in RosterListAdapter, we may as well have it handle the model modifications as well. However, our RosterRowHolder does not have access to the RosterListAdapter. Oddly, there is no method on RecyclerView.ViewHolder to return the RecyclerView.Adapter that created the ViewHolder. So, we have to track that ourselves. Add a RosterListAdapter field, named adapter, to RosterRowHolder, and add a constructor parameter to initialize it: private final RosterListAdapter adapter; public RosterRowHolder(TodoRowBinding binding, RosterListAdapter adapter) { super super(binding.getRoot()); this this.binding=binding; this this.adapter=adapter; } (from T16-Completion/ToDo/app/src/main/java/com/commonsware/todo/RosterRowHolder.java)

Over in RosterListAdapter, change onCreateViewHolder() to pass in the RosterListAdapter itself to the RosterRowHolder constructor: @Override public RosterRowHolder onCreateViewHolder(ViewGroup parent, int viewType) { TodoRowBinding binding= TodoRowBinding.inflate(host.getLayoutInflater(), parent, false false); return new RosterRowHolder(binding, this this); } (from T16-Completion/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

Then, add the completeChanged() to RosterRowHolder that we are calling from our layoutâ&#x20AC;&#x2122;s binding expression:

186


TRACKING THE COMPLETION STATUS public void completeChanged(ToDoModel model, boolean isChecked) { if (model.isCompleted()!=isChecked) { adapter.replace(model, isChecked); } (from T16-Completion/ToDo/app/src/main/java/com/commonsware/todo/RosterRowHolder.java)

Here, we confirm that we have a different is-completed state, and if we do, we call a replace() method on RosterListAdapterâ&#x20AC;Ś that does not yet exist. Fortunately, there is another step to go in this tutorial.

Step #4: Saving the Change Now we need to implement replace() on RosterListAdapter, connecting it with the replace() method that we put on ToDoRepository in the preceding tutorial. However, if we are going to start working with the ToDoRepository a lot, it would be helpful to hold onto it in a field, for simplicity. So, in RosterListAdapter, add a repo field for our ToDoRepository and modify the RosterListAdapter constructor to initialize that field: final private ToDoRepository repo; RosterListAdapter(RosterListFragment host) { this this.repo=ToDoRepository.get(); models=repo.all(); this this.host=host; } (from T16-Completion/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

Then, add this replace() implementation to RosterListAdapter: public void replace(ToDoModel model, boolean isChecked) { repo.replace(model.toBuilder().isCompleted(isChecked).build()); } (from T16-Completion/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

We create a new ToDoModel with the change from the preceding one, then replace() the old model with the new one in the repository.

187


TRACKING THE COMPLETION STATUS If you run this revised sample… nothing much seems to change. The UI itself is unaffected, and you cannot readily see the objects in the repository.

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • app/src/main/res/layout/todo_row.xml • app/src/main/java/com/commonsware/todo/RosterRowHolder.java • app/src/main/java/com/commonsware/todo/RosterListAdapter.java

188


Displaying an Item

We are storing things, like notes, in the ToDoModel that do not appear in the roster list. That sort of list usually shows limited information, with the rest of the details shown when you tap on an item in the list. That is the approach that we will use here, where we will show a separate fragment with the details of the to-do item when the user taps on the item. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Step #1: Creating the Fragment Once again, we need to set up a fragment. Right-click over the com.commonsware.todo package in the java/ directory and choose “New” > “Java Class” from the context menu. This will bring up a dialog where we can define a new Java class. For the name, fill in DisplayFragment. For the superclass, fill in android.support.v4.app.Fragment. Click “OK” to create the class. That will give you a DisplayFragment that looks like: package com.commonsware.todo; import android.support.v4.app.Fragment android.support.v4.app.Fragment; public class DisplayFragment extends Fragment { }

189


DISPLAYING AN ITEM

Step #2: Instantiating the Fragment At some point, we need to create an instance of DisplayFragment in order to… well… display it. However, DisplayFragment needs to know the ID of the to-do item that it is supposed to display. We need a way to get that ID over to the fragment. For a regular Java class, this would be a matter of using a constructor parameter or a setter method. However, those options are not great in this case: • A fragment needs a zero-argument public constructor, as the Android framework requires it • We need to consider the impacts of configuration changes, such as the user rotating the screen, and an ordinary field with a setter method will not handle that well • We need to consider the impacts of our process being terminated while we are in the background (e.g., the user went off and did something else for 10 minutes, then returned to our app), and an ordinary field with a setter method will not handle that well The recommended approach with fragments is to use the factory pattern, where a newInstance() method is responsible for setting up the fragment. The to-do item’s ID would be passed into that method. newInstance(), in turn, uses the “arguments” Bundle to associate that ID with the fragment. A Bundle is a HashMap-like structure, with keys and values. The arguments Bundle holds its values across configuration changes and short-term process termination scenarios. With all of that in mind, add a constant named ARG_ID to DisplayFragment: private static final String ARG_ID="id"; (from T17-Display/ToDo/app/src/main/java/com/commonsware/todo/DisplayFragment.java)

Then, add this newInstance() implementation to DisplayFragment: static DisplayFragment newInstance(ToDoModel model) { DisplayFragment result=new new DisplayFragment(); if (model!=null null) { Bundle args=new new Bundle(); args.putString(ARG_ID, model.id());

190


DISPLAYING AN ITEM result.setArguments(args); } return result; } (from T17-Display/ToDo/app/src/main/java/com/commonsware/todo/DisplayFragment.java)

Here, we pass in the entire ToDoModel representing the particular to-do item to display. We only need the ID, though, as: • We can get the actual ToDoModel back from the repository as needed • We cannot put a ToDoModel in the Bundle without additional work that we do not need So, here we: • Create an instance of DisplayFragment • Create a Bundle • Put the ID of the ToDoModel into the Bundle, using that ARG_ID constant as the key • Attach the Bundle to the fragment via setArguments() • Return the resulting DisplayFragment

Step #3: Responding to List Clicks The typical way lists work in Android is that if you tap on a row in the list, the user is taken to some UI (activity or fragment) that pertains to the tapped-upon row. More generally, we have ways of finding out when the row gets clicked upon. Right now, though, that will not work in our app. If you run the app, any you try tapping anywhere on the row, the CheckBox state toggles between checked and unchecked. That is because our CheckBox completely fills the row, and a CheckBox interprets a click anywhere as a trigger to toggle the CheckBox state. This is a problem. There is no good way to tell CheckBox to only toggle the state when the user clicks on the box itself, not the text. We do not even have a good way to detect if the user clicked the text instead of the box. 191


DISPLAYING AN ITEM So, we need to reorganize our list row a little bit, using a CheckBox without any text, plus a TextView for the label. Then we can find out when the user clicks on that label, and use that as our trigger to go display that to-do item. Open res/layout/todo_row.xml in Android Studio. The layout as it stands should resemble: <?xml version="1.0" encoding="utf-8"?> <layout> <data> <variable name="model" type="com.commonsware.todo.ToDoModel" /> <variable name="holder" type="com.commonsware.todo.RosterRowHolder" /> </data> <android.support.constraint.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="wrap_content"> > <CheckBox android:id="@+id/desc" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:checked="@{model.isCompleted()}" android:ellipsize="end" android:maxLines="3" android:onCheckedChanged="@{(cb, isChecked) -> holder.completeChanged(model, isChecked)}" android:text="@{model.description()}" android:textSize="@dimen/desc_size" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />

192


DISPLAYING AN ITEM </android.support.constraint.ConstraintLayout> </layout>

Unfortunately, due to the Android Studio bug mentioned in the preceding chapter, we cannot manipulate this layout in the graphical layout editor so long as that android:onCheckedChanged attribute is there. So, in the “Text” sub-tab with the XML, remove that attribute for the time being — we will restore it later in this step. The CheckBox is set to a width of 0dp, so it fills the screen. Either in the XML or in the Attributes pane, modify the android:layout_width of the CheckBox to be wrap_content, and eliminate the constraint tying the end of the CheckBox to the end of its parent (i.e., remove app:layout_constraintEnd_toEndOf="parent"). Also, change the ID of the CheckBox to be checkbox (android:id="@+id/checkbox").

Figure 121: Android Studio Designer, Showing Smaller CheckBox Then, add a TextView widget to the ConstraintLayout. Using the drag-and-drop editor, add a constraint from the starting edge of the TextView to the end of the CheckBox, and add a constraint from the ending edge of the TextView to the end of its parent:

193


DISPLAYING AN ITEM

Figure 122: Android Studio Designer, Showing Added TextView Then, set the layout_width to be match_constraint (a.k.a., 0dp):

Figure 123: Android Studio Designer, Showing Larger TextView Then, create constraints to tie the TextView to the top and bottom of its parent:

Figure 124: Android Studio Designer, Showing Constrained TextView Give the TextView an ID of desc (android:id="@+id/desc"). Then, move the android:text="@{model.description()}" attribute from the CheckBox to the TextView, so our description shows up on the TextView. Do the same for the android:ellipsize, android:maxLines, and android:textSize attributes. Also, now you can restore this attribute to the CheckBox: android:onCheckedChanged="@{(cb, isChecked) -> holder.completeChanged(model, isChecked)}"

This should leave you with a layout that resembles: <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> >

194


DISPLAYING AN ITEM <data> <variable name="model" type="com.commonsware.todo.ToDoModel" /> <variable name="holder" type="com.commonsware.todo.RosterRowHolder" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content"> > <CheckBox android:id="@+id/checkbox" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:checked="@{model.isCompleted()}" android:onCheckedChanged="@{(cb, isChecked) -> holder.completeChanged(model, isChecked)}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/desc" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:ellipsize="end" android:maxLines="3" android:text="@{model.description()}" android:textSize="@dimen/desc_size" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/checkbox" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>

195


DISPLAYING AN ITEM If you run the app, you should see that the rows look more or less as they did before, but nothing happens when you click on the text. The CheckBox state only toggles if you click on the box. As with the onCheckedChanged event on the CheckBox, we want to route the click events on our rows to our RosterRowHolder. So, add a stub onClick() method to RosterRowHolder: public void onClick() { }

Then, add an android:onClick attribute to the TextView to route clicks on it to the holder variable: android:onClick="@{() -> holder.onClick()}" (from T17-Display/ToDo/app/src/main/res/layout/todo_row.xml)

Step #4: Displaying the (Empty) Fragment Now that we can get control when the user taps on a row, we need to show that DisplayFragment… even if at the moment it will be empty. However, we should have the MainActivity display the fragment, for a few reasons: • Only the activity knows where to display the fragment: taking over the whole content area, or only part of it on a larger-screen device? • We may want to display this fragment from multiple different places in the code, so having a central “point of control” for displaying the fragment will be useful • We may need to consider how to handle the back stack — what happens when the user presses the BACK button — and the MainActivity is in the best position to know what needs to happen when So, we need to get control to MainActivity. We could cheat. All widgets have a getContext() method. In many cases, that Context is the Activity that hosts the widget (directly or via a fragment intermediary). So we could get a widget from our row layout — such as the CheckBox, call getContext() on that, cast that to be MainActivity, and call methods on it. However, getContext() is not required to return the activity. It could return

196


DISPLAYING AN ITEM something else, such as a ContextWrapper. So, to be safe, we will ripple the event upwards until we are sure that we are going to be getting it to MainActivity and not run into a ClassCastException somewhere along the way. Let’s work downwards from the MainActivity. Add this stub method to MainActivity: public void showModel(ToDoModel model) { }

We will aim to call that method with the to-do item that needs to be displayed. Our row is managed by the RosterListFragment. A fragment knows the activity that hosts it, and we can call getActivity() to retrieve that activity. A typical way of implementing that is via a “contract” interface, where the fragment publishes an interface that hosting activities are required to implement. That way, the fragment does not need to know the exact class of the activity that is hosting it — it merely knows that the activity is supposed to implement this interface. So, on RosterListFragment, add this inner interface declaration: interface Contract { void showModel(ToDoModel model); } (from T17-Display/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

That happens to line up with the method we just added on MainActivity. However, MainActivity technically does not implement the Contract interface. So, modify MainActivity to say that it implements Contract: public class MainActivity extends FragmentActivity implements RosterListFragment.Contract { (from T17-Display/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

Then add the @Override annotation to showModel() on MainActivity, so we advertise to the compiler that we are intending to implement showModel() from a superclass or interface definition: @Override public void showModel(ToDoModel model) { }

197


DISPLAYING AN ITEM Next, on RosterListAdapter, add this method: void showModel(ToDoModel model) { ((RosterListFragment.Contract)host.getActivity()).showModel(model); } (from T17-Display/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

Here, we: • Call getActivity() on the RosterListFragment • Cast that to the Contract interface • Call showModel() on the Contract, passing along a ToDoModel to be displayed So now, all that we need to do is call showModel() on the RosterListAdapter from the onClick() method of the RosterRowHolder. We have to pass in the ToDoModel instance that is supposed to be displayed. We provided that ToDoModel to the TodoRowBinding in the bind() method on RosterRowHolder. TodoRowBinding not only has a setter method for the ToDoModel, but also the corresponding getter method. With that in mind, modify onClick() on RosterRowHolder to be: public void onClick() { adapter.showModel(binding.getModel()); } (from T17-Display/ToDo/app/src/main/java/com/commonsware/todo/RosterRowHolder.java)

Now, we have sent along the event and the affected to-do item to the MainActivity. Finally, we need to actually display the DisplayFragment. Modify showModel() in MainActivity to run another FragmentTransaction: @Override public void showModel(ToDoModel model) { getSupportFragmentManager().beginTransaction() .replace(android.R.id.content, DisplayFragment.newInstance(model)) .addToBackStack(null null) .commit(); } (from T17-Display/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

198


DISPLAYING AN ITEM Here, we are putting the DisplayFragment into the main content area (android.R.id.content), just as we did with the RosterListFragment. We are using the newInstance() factory method that we created earlier. And we are using addToBackStack() to say that when the user presses BACK from the DisplayFragment, rather than exiting the activity entirely, we should return to the fragment that preceded it, which in this case is the RosterListFragment. If you run the sample app now, and you click on one of the to-do items, you will be taken to the DisplayFragment… which happens to not display anything. We will fix that in the upcoming steps of this tutorial. If you press BACK when viewing the (empty) DisplayFragment, you will return to the list of to-do items.

Step #5: Creating an Empty Layout To have DisplayFragment display the contents of a ToDoModel, it helps to have a layout resource. Right-click over the res/layout/ directory and choose “New” > “Layout resource file” from the context menu. In the dialog that appears, fill in todo_display as the “File name” and ensure that the “Root element” is set to android.support.constraint.ConstraintLayout (which should be the default value). Then, click “OK” to close the dialog and create the mostly-empty resource file.

Step #6: Setting Up Data Binding As with the roster rows, we are going to use data binding to populate the widgets. That means that we need to adjust the layout to contain the <layout> root tag and a <data> element for our variables. Since we know that we are going to show a ToDoModel, we can go ahead and set up that variable as well. In the Text sub-tab of the todo_display editor, modify the layout to look like this: <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> > <data> <variable

199


DISPLAYING AN ITEM name="model" type="com.commonsware.todo.ToDoModel" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> > </android.support.constraint.ConstraintLayout> </layout>

This sets up a model variable, the same as we used in the roster rows.

Step #7: Adding the Completed Icon Part of what we need to display is whether or not this to-do item is completed. In the roster rows, that was handled by the CheckBox. However, a CheckBox is a widget designed for input. We have two choices: 1. We could use a CheckBox and allow the user to change the completion status from with the DisplayFragment 2. We could use something else to represent the current completion status, restricting the user to changing the status somewhere else Since we are also going to allow the user to change the completion status from the fragment that allows editing of the whole ToDoModel, it seems reasonable to make DisplayFragment be display-only. We could still use a CheckBox and simply ignore any changes that the user makes in it, but that’s just rude. Instead, let’s use an ImageView to display an icon for completed items, hiding it for items that are not completed. To do this, first we should set up the icon artwork. Right-click over res/drawable/ in the project tree and choose “New” > “Vector Asset” from the context menu. This brings up the Vector Asset Wizard, that we used when creating the action bar item in an earlier tutorial. There, click the “Icon” button and search for check:

200


DISPLAYING AN ITEM

Figure 125: Android Studio Vector Asset Selector, Showing “check” Options Choose the “check circle” icon and click “OK” to close up the icon selector. Then, click “Next” and “Finish” to close up the wizard and set up our icon. Then, switch over to the Design sub-tab in the layout editor and drag an ImageView from the “Widgets” category of the Palette into the layout. This immediately displays a dialog from which you can choose a resource to display:

201


DISPLAYING AN ITEM

Figure 126: Android Studio Drawable Resource Selector Open the “Project” category and choose the ic_check_circle_black_24dp resource that we just created, then click “OK” to close up the dialog. Add constraints to tie the top and end side of the ImageView to the top and end side of the ConstraintLayout:

Figure 127: Android Studio Layout Designer, Showing Tiny ImageView In the Attributes pane, give the ImageView an “ID” of completed. The icon is a bit small by default, at only 24dp. We can make it bigger by changing its width and height. We want it to be square, and we might want the size to be different on larger-screen devices. So, we should set up a dimension resource for a larger size, then apply it to both the width and the height.

202


DISPLAYING AN ITEM To do that, click the “…” button to the right of the “layout_width” drop-down in the Attributes pane:

Figure 128: Android Studio Layout Designer, Showing Tiny “…” Button That brings up a dialog to choose a dimension resource:

Figure 129: Android Studio Dimension Resource Selector From the drop-down in the corner, choose “Add new resource” > “New dimen Value…”, to bring up the new-resource dialog:

203


DISPLAYING AN ITEM

Figure 130: Android Studio New Dimension Resource Dialog For the name, fill in checked_icon_size, and use 48dp for the value:

204


DISPLAYING AN ITEM

Figure 131: Android Studio New Dimension Resource Dialog, Filled In Click “OK” to close up both dialogs and fill in that dimension. Then, click the “…” next to the “layout_height” drop-down in the Attributes pane, and choose the checked_icon_size dimension from the list. This will give you a larger icon, with both height and width set to 48dp:

Figure 132: Android Studio Layout Designer, Showing Slightly Larger ImageView

205


DISPLAYING AN ITEM For accessibility, it is good to supply a “content description” for an ImageView, which is some text to announce using a screen reader. To do that, click the “…” button next to the “contentDescription” field, to bring up a string resource chooser. From the drop-down in the corner, choose “Add new resource” > “New string Value…”. For the resource name, use is_completed, and for the resource value, use Item is completed. Click “OK” to close up both dialogs and apply this new string resource to the android:contentDescription attribute. The icon appears black. That works, but it is a bit boring, and it does not blend in with the rest of the colors used in the app. To change it to use our accent color, switch the Attributes pane to show all attributes, then find the “tint” attribute. If you click that row in the attributes table, a “…” button should appear. Click that to bring up a color resource chooser. Open the “Project” category, then double-click on the accent color resource, to close up the dialog and apply that tint to the icon:

Figure 133: Android Studio Layout Designer, Showing Tinted ImageView Finally, we only want to show this icon when the to-do item is completed. Otherwise, is should not be visible. To do that, switch over to the “Text” sub-tab of the editor to work with the XML. Add the following attribute to the ImageView: android:visibility="@{model.completed ? View.VISIBLE : View.GONE}" (from T17-Display/ToDo/app/src/main/res/layout/todo_display.xml)

This is a binding expression that uses a Java-style ternary expression. We check to see if the ToDoModel is completed — completed in the binding expression language will map to the isCompleted() getter method. If it is true, we assign View.VISIBLE to the visibility, otherwise we assign View.GONE. This implements what we want: show the icon for completed items, and hide it for others. 206


DISPLAYING AN ITEM However, in order to be able to reference View constants, we need to add an <import> element to the <data> element: <import type="android.view.View" /> (from T17-Display/ToDo/app/src/main/res/layout/todo_display.xml)

This works like a Java import statement, indicating where the View symbol comes from. Your layout XML should now resemble: <?xml version="1.0" encoding="utf-8"?> <layout xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"> > <data> <variable name="model" type="com.commonsware.todo.ToDoModel" /> <import type="android.view.View" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> > <ImageView android:id="@+id/completed" android:layout_width="@dimen/checked_icon_size" android:layout_height="@dimen/checked_icon_size" android:layout_marginEnd="8dp" android:layout_marginTop="8dp" android:contentDescription="@string/is_completed" android:src="@drawable/ic_check_circle_black_24dp" android:tint="@color/accent" android:visibility="@{model.completed ? View.VISIBLE : View.GONE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>

207


DISPLAYING AN ITEM

Step #8: Displaying the Description Next is the description of the to-do item. This should be fairly large and prominent, as it is the main piece of information that the user is going to want to see. Switch back to the Design sub-tab of the layout resource editor. From the “Text” category of the Palette pane, drag a TextView into the preview area. Using the grab handles, set up three constraints: • Tie the top and start edges to the corresponding edges of the ConstraintLayout

• Tie the end edge to the start edge of the ImageView

Figure 134: Android Studio Layout Designer, Showing Added TextView Change the “layout_width” attribute to match_constraint (a.k.a., 0dp): Also, give the widget an “ID” of desc.

Figure 135: Android Studio Layout Designer, Showing Stretched TextView Switch the Attributes pane to show all of the attributes. Click on the “textSize” row, then click the “…” button that appears in that row. As we did above, define a new dimension resource, named desc_view_size, set to 24sp. Clicking “OK” to exit the dialog will also apply that size to the widget. 208


DISPLAYING AN ITEM Just below that is “textStyle”. Fold it open and choose “bold” from the checklist:

Figure 136: Android Studio Layout Designer, Showing Bigger, Bolder TextView Finally, in the XML, replace the android:text attribute with the same binding expression that we used in todo_roster.xml: android:text="@{model.description()}" (from T17-Display/ToDo/app/src/main/res/layout/todo_display.xml)

The layout should now resemble: <?xml version="1.0" encoding="utf-8"?> <layout xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"> > <data> <variable name="model" type="com.commonsware.todo.ToDoModel" /> <import type="android.view.View" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> > <ImageView android:id="@+id/completed" android:layout_width="@dimen/checked_icon_size" android:layout_height="@dimen/checked_icon_size"

209


DISPLAYING AN ITEM android:layout_marginEnd="8dp" android:layout_marginTop="8dp" android:contentDescription="@string/is_completed" android:src="@drawable/ic_check_circle_black_24dp" android:tint="@color/accent" android:visibility="@{model.completed ? View.VISIBLE : View.GONE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/description" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:text="@{model.description()}" android:textSize="@dimen/desc_view_size" android:textStyle="bold" app:layout_constraintEnd_toStartOf="@+id/completed" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>

Step #9: Showing the Created-On Date The next bit of data to display is the date on which the to-do item was created. This differs from how we handle the description in two ways: 1. We need to provide a label for the date, as otherwise the user may not realize what this date means 2. We need to format the value in a format that the user will understand (versus, say, showing the number of milliseconds since the Unix epoch) First, let’s set up the label. In the “Design” sub-tab, drag another TextView into the layout. Drag the grab handles to set up constraints: • On the start end of the label, to the start side of the ConstraintLayout • On the top of the label, to the bottom of the desc TextView

210


DISPLAYING AN ITEM

Figure 137: Android Studio Layout Designer, Showing Another Added TextView Give the widget an ID of label_created. To set the text to a fixed value, we can set up another string resource. However, the Attributes pane has two attributes that look like they set the text of the TextView:

Figure 138: Android Studio Layout Designer, Showing Two TextView Text Options The one with the wrench icon sets up separate text to show when working in the design view of the layout resource editor. We want the other one, that sets the text for actual app — it has TextView as the value right now. Click the “…” button next to that field, and choose the drop-down option to create a new string resource. Give it a name of created_on and a value of “Created on:”. Clicking “OK” will close the dialog and assign that string resource to the TextView for the android:text attribute. Switch the Attributes pane to view all of the attributes, and find the textSize attribute. As before, click the “…” next to that field, and choose to create a new dimension resource from the drop-down. Give it a name of created_on_size and a value of 16sp. Clicking “OK” will close the dialog and assign that dimension resource to the android:textSize attribute. Now, we can show the created-on date, next to our newly-created label. Drag yet another TextView into the layout. Drag the grab handles to set up constraints: • On the start side of this TextView, to the end side of the label_created TextView

• On the top of this TextView, to the bottom of the desc TextView • On the end side of this TextView, to the start side of the icon

211


DISPLAYING AN ITEM

Figure 139: Android Studio Layout Designer, Showing Yet Another TextView Then, set the “layout_width” to match_constraint (a.k.a., 0dp):

Figure 140: Android Studio Layout Designer, Showing Stretched TextView Give the widget an ID of created_on. In the full list of attributes, click the “…” next to the “textSize” attribute, and choose the created_on_size resource that we added previously. Finally, we need to set the text of this TextView to the created-on date. We need to format the date. That sort of thing is the responsibility of the class that is using the layout resource, which in this case is RosterRowHolder. We can add another variable to have RosterRowHolder bind in a formatted string to display here. So, switch over to the “Text” sub-tab to work with the XML, and add another <variable> element to the <data> element: <variable name="createdOn" type="java.lang.CharSequence" /> (from T17-Display/ToDo/app/src/main/res/layout/todo_display.xml)

We could use java.lang.String here, but java.lang.CharSequence is more flexible — in Android, strings with inline formatting (e.g., italics) are of type CharSequence. Then, use a binding expression to tie the createdOn variable to the newly-added TextView: android:text="@{createdOn}"

212


DISPLAYING AN ITEM (from T17-Display/ToDo/app/src/main/res/layout/todo_display.xml)

At this point, the XML of the layout resource should resemble: <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> > <data> <variable name="model" type="com.commonsware.todo.ToDoModel" /> <variable name="createdOn" type="java.lang.CharSequence" /> <import type="android.view.View" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> > <ImageView android:id="@+id/completed" android:layout_width="@dimen/checked_icon_size" android:layout_height="@dimen/checked_icon_size" android:layout_marginEnd="8dp" android:layout_marginTop="8dp" android:contentDescription="@string/is_completed" android:src="@drawable/ic_check_circle_black_24dp" android:tint="@color/accent" android:visibility="@{model.completed ? View.VISIBLE : View.GONE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/description" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:text="@{model.description()}" android:textSize="@dimen/desc_view_size" android:textStyle="bold"

213


DISPLAYING AN ITEM app:layout_constraintEnd_toStartOf="@+id/completed" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/label_created" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:text="@string/created_on" android:textSize="@dimen/created_on_size" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/description" /> <TextView android:id="@+id/created_on" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:text="@{createdOn}" android:textSize="@dimen/created_on_size" app:layout_constraintEnd_toStartOf="@+id/completed" app:layout_constraintStart_toEndOf="@+id/label_created" app:layout_constraintTop_toBottomOf="@+id/description" /> </android.support.constraint.ConstraintLayout> </layout>

Step #10: Adding the Notes There is only one more widget to add: another TextView, this time for the notes. Over in the design tab, drag one more TextView out of the Palette pane into the preview area. Set the constraints to have the top of the TextView attach to the bottom of the created_on TextView, and have the other three sides attach to the edges of the ConstraintLayout:

214


DISPLAYING AN ITEM

Figure 141: Android Studio Layout Designer, Showing One More TextView Change both the “layout_width” and “layout_height” attributes to match_constraint:

Figure 142: Android Studio Layout Designer, Showing Stretched TextView Give the widget an ID of notes. 215


DISPLAYING AN ITEM In the all-attributes list, scroll down to “textSize”. Click the “…” next to that field, and elect to create a new dimension resource from the drop-down menu. Give it a value of notes_size and a value of 20sp. Clicking “OK” will close the dialog and assign that dimension resource to the android:textSize attribute. Finally, in the “Text” sub-tab, use a binding expression to tie the notes from the ToDoModel to the android:text attribute: android:text="@{model.notes()}" (from T17-Display/ToDo/app/src/main/res/layout/todo_display.xml)

At this point, your layout XML should resemble: <?xml version="1.0" encoding="utf-8"?> <layout xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"> > <data> <variable name="model" type="com.commonsware.todo.ToDoModel" /> <variable name="createdOn" type="java.lang.CharSequence" /> <import type="android.view.View" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> > <ImageView android:id="@+id/completed" android:layout_width="@dimen/checked_icon_size" android:layout_height="@dimen/checked_icon_size" android:layout_marginEnd="8dp" android:layout_marginTop="8dp" android:contentDescription="@string/is_completed" android:src="@drawable/ic_check_circle_black_24dp" android:tint="@color/accent" android:visibility="@{model.completed ? View.VISIBLE : View.GONE}"

216


DISPLAYING AN ITEM app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/desc" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:text="@{model.description()}" android:textSize="@dimen/desc_view_size" android:textStyle="bold" app:layout_constraintEnd_toStartOf="@+id/completed" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/label_created" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:text="@string/created_on" android:textSize="@dimen/created_on_size" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/desc" /> <TextView android:id="@+id/created_on" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:text="@{createdOn}" app:layout_constraintEnd_toStartOf="@+id/completed" app:layout_constraintStart_toEndOf="@+id/label_created" app:layout_constraintTop_toBottomOf="@+id/desc" /> <TextView android:id="@+id/notes" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp"

217


DISPLAYING AN ITEM android:text="@{model.notes()}" android:textSize="@dimen/notes_size" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/label_created" /> </android.support.constraint.ConstraintLayout> </layout> (from T17-Display/ToDo/app/src/main/res/layout/todo_display.xml)

Step #11: Populating the Layout Now, we can return to our Java code, specifically the DisplayFragment, and hook it up to this newly-created layout resource. In DisplayFragment, add a binding field, pointing to our newly-generated TodoDisplayBinding class from our todo_display layout resource: private TodoDisplayBinding binding; (from T17-Display/ToDo/app/src/main/java/com/commonsware/todo/DisplayFragment.java)

Then, in DisplayFragment, add an onCreateView() method: @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding=TodoDisplayBinding.inflate(getLayoutInflater(), container, false false); return binding.getRoot(); } (from T17-Display/ToDo/app/src/main/java/com/commonsware/todo/DisplayFragment.java)

This works akin to how onCreateViewHolder() does in RosterListAdapter, inflating the binding from the resource, using the code-generated TodoDisplayBinding class. In order to be able to bind the ToDoModel, we need the model object. To get the model object, we need its ID from the arguments Bundle. So, add the following method to DisplayFragment:

218


DISPLAYING AN ITEM private String getModelId() { return getArguments().getString(ARG_ID); } (from T17-Display/ToDo/app/src/main/java/com/commonsware/todo/DisplayFragment.java)

This simply pulls the model ID string out of the arguments Bundle, using the ARG_ID key that we used to store the ID in the Bundle in the first place. We need some way to get the model for this ID. We can do that by adding another method to ToDoRepository, named find(): public ToDoModel find(String id) { for (ToDoModel candidate: all()) { if (candidate.id().equals(id)) { return candidate; } } return null null; } (from T17-Display/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

This just iterates over the models to find the one with the ID. Eventually, this will be replaced with a database-backed solution. Then, back in DisplayFragment, add this onViewCreated() method: @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super super.onViewCreated(view, savedInstanceState); ToDoModel model=ToDoRepository.get().find(getModelId()); binding.setModel(model); binding.setCreatedOn(DateUtils.getRelativeDateTimeString(getActivity(), model.createdOn().getTimeInMillis(), DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0)); } (from T17-Display/ToDo/app/src/main/java/com/commonsware/todo/DisplayFragment.java)

This retrieves the model given its ID and binds it to the layout. It also formats the created-on date, using DateUtils.getRelativeDateTimeString(), which will return

219


DISPLAYING AN ITEM a value formatted in accordance with the user’s locale and device configuration, plus use a relative time (e.g., “35 minutes ago”) for recent times. At this point, if you run the app, and you click on one of the to-do items in the list, the full details should appear in the DisplayFragment:

Figure 143: ToDo App, Displaying Item Details Pressing BACK returns you to the list, as before.

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • • • • • •

app/src/main/java/com/commonsware/todo/DisplayFragment.java app/src/main/res/layout/todo_row.xml app/src/main/java/com/commonsware/todo/RosterRowHolder.java app/src/main/java/com/commonsware/todo/MainActivity.java app/src/main/java/com/commonsware/todo/RosterListFragment.java app/src/main/java/com/commonsware/todo/RosterListAdapter.java

220


DISPLAYING AN ITEM • app/src/main/res/layout/todo_display.xml • app/src/main/res/drawable/ic_check_circle_black_24dp.xml • app/src/main/java/com/commonsware/todo/ToDoRepository.java

221


Editing an Item

Displaying to-do items is nice. However, right now, all of the to-do items are fake. We need to start allowing the user to fill in their own to-do items. The first task is to set up an edit fragment. Just as we can click on a to-do item in the list to bring up the details, we need to be able to click on something in the details to be able to edit the description, notes, etc. So, just as we created a DisplayFragment in the preceding tutorial, here we will create an EditFragment and arrange to display it. This tutorial has many similarities to the preceding one: • We create a fragment • We create a layout for that fragment • We use data binding to populate the layout from the fragment The differences come in the layout itself, as we have a different mix of widgets than before. Plus, we need to add an icon to the DisplayFragment, to allow the user to request to edit that to-do item. You might wonder “hey, shouldn’t we use inheritance or something here?” In theory, we could. In practice, the DisplayFragment is going to change quite a bit in a later tutorial, and so we would have to undo the inheritance work at that point anyway. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

223


EDITING AN ITEM

Step #1: Creating the Fragment For the third and last time, we need to set up a fragment. Right-click over the com.commonsware.todo package in the java/ directory and choose “New” > “Java Class” from the context menu. This will bring up a dialog where we can define a new Java class. For the name, fill in EditFragment. For the superclass, fill in android.support.v4.app.Fragment. Click OK to create the class. That will give you an EditFragment that looks like: package com.commonsware.todo; import android.support.v4.app.Fragment android.support.v4.app.Fragment; public class EditFragment extends Fragment { }

Step #2: Instantiating the Fragment This class needs the ID of the ToDoModel to edit, just as DisplayFragment needed the model to display. So, we are going to set up the same sort of code in EditFragment that we have in DisplayFragment, in terms of the factory method and the getModelId() method. Add a constant named ARG_ID to EditFragment: private static final String ARG_ID="id"; (from T18-Edit/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

Then, add this newInstance() implementation to EditFragment: static EditFragment newInstance(ToDoModel model) { EditFragment result=new new EditFragment(); if (model!=null null) { Bundle args=new new Bundle(); args.putString(ARG_ID, model.id()); result.setArguments(args); }

224


EDITING AN ITEM return result; } (from T18-Edit/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

And, add this getModelId() implementation to EditFragment, akin to the one from DisplayFragment: private String getModelId() { return getArguments().getString(ARG_ID); } (from T18-Edit/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

Step #3: Setting Up a Menu Resource Somewhere, somehow, the user has to be able to get to this fragment. A typical pattern is for there to be an “edit” option somewhere where we are displaying the thing to be edited. In the case of this app, that implies having an “edit” option on the DisplayFragment, and we can do this by adding a action bar item. First, though, we need an icon for that button. Right-click over res/drawable/ in the project tree and choose “New” > “Vector Asset” from the context menu. This brings up the Vector Asset Wizard. There, click the “Icon” button and search for edit:

225


EDITING AN ITEM

Figure 144: Android Studio Vector Asset Selector, Showing “edit” Options Choose the “edit” icon and click “OK” to close up the icon selector. Then, click “Next” and “Finish” to close up the wizard and set up our icon. Then, right-click over the res/menu/ directory and choose New > “Menu resource file” from the context menu. Fill in actions_display.xml in the “New Menu Resource File” dialog, then click OK to create the file and open it up in the menu editor. In the Palette, drag a “Menu Item” into the preview area. This will appear as an item in an overflow area:

Figure 145: Android Studio Menu Editor, Showing Added MenuItem

226


EDITING AN ITEM In the Attributes pane, fill in edit for the “id”. Then, choose both “ifRoom” and “withText” for the “showAsAction” option:

Figure 146: Android Studio Menu Editor, Attributes Pane, Showing “showAsAction” Popup Click on the “…” button next to the “icon” field. This will bring up an drawable resource selector. Open the “Project” category, then click on ic_edit_black_24dp in the list of drawables, then click OK to accept that choice of icon. Then, click the “…” button next to the “title” field. As before, this brings up a string resource selector. Click on “Add new resource” > “New string Value” in the dropdown towards the top. In the dialog, fill in menu_edit as the resource name and “Edit” as the resource value:

227


EDITING AN ITEM

Figure 147: Android Studio New String Resource Dialog Click OK to close the dialog. At this point, your menu editor preview should resemble:

Figure 148: Android Studio Menu Editor, Showing Configured MenuItem …and the menu resource itself should have this XML in the “Text” sub-tab: <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> > <item android:id="@+id/edit"

228


EDITING AN ITEM android:icon="@drawable/ic_edit_black_24dp" android:showAsAction="ifRoom|withText" android:title="@string/menu_edit" /> </menu> (from T18-Edit/ToDo/app/src/main/res/menu/actions_display.xml)

Step #4: Showing the Action Item We also need to take steps to arrange to show this action bar item on DisplayFragment. Previously, we defined an action bar item that would be available to the entire activity. Now we want one that will be for just this one fragment. The way to do that is to have the fragment itself add this item to the action bar — Android will only show this item when the fragment itself is visible. Add this onCreate() method to DisplayFragment: @Override public void onCreate(@Nullable Bundle savedInstanceState) { super super.onCreate(savedInstanceState); setHasOptionsMenu(true true); } (from T18-Edit/ToDo/app/src/main/java/com/commonsware/todo/DisplayFragment.java)

onCreate()

is called when the fragment is created, and here we indicate that we want to add items to the action bar, via setHasOptionsMenu(true). Next, add this onCreateOptionsMenu() method to DisplayFragment: @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.actions_display, menu); super super.onCreateOptionsMenu(menu, inflater); } (from T18-Edit/ToDo/app/src/main/java/com/commonsware/todo/DisplayFragment.java)

Here, we use a MenuInflater to “inflate” the menu resource and add its item to the action bar. Plus, we chain to the superclass, in case the superclass wants to add things to the action bar as well.

229


EDITING AN ITEM If you run the app and tap on a to-do item in the list, you should see the new action bar item on the DisplayFragment:

Figure 149: ToDo App, DisplayFragment, with Edit Action Bar Item

Step #5: Displaying the (Empty) Fragment Now that we are displaying the action bar item, we can get control and show the presently-empty EditFragment. As before, we will have MainActivity be responsible for displaying the fragment, as the rules for how it does so will vary based on circumstances (e.g., large-screen device in master-detail mode versus a smaller device). We will use the same approach as before, with a Contract interface that the activity will implement and the fragment will call. As we did in the previous tutorial, letâ&#x20AC;&#x2122;s work downwards from the MainActivity. Add this stub method to MainActivity: public void editModel(ToDoModel model) { }

On DisplayFragment, add this inner interface declaration:

230


EDITING AN ITEM interface Contract { void editModel(ToDoModel model); } (from T18-Edit/ToDo/app/src/main/java/com/commonsware/todo/DisplayFragment.java)

Then, modify MainActivity to say that it implements both this Contract and the previous Contract from ToDoListRosterFragment: public class MainActivity extends FragmentActivity implements RosterListFragment.Contract, DisplayFragment.Contract { (from T18-Edit/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

Then add the @Override annotation to editModel() on MainActivity, so we advertise to the compiler that we are intending to implement editModel() from a superclass or interface definition: @Override public void editModel(ToDoModel model) { }

In DisplayFragment, add this method: @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId()==R.id.edit) { ((Contract)getActivity()).editModel(binding.getModel()); return true true; } return super super.onOptionsItemSelected(item); } (from T18-Edit/ToDo/app/src/main/java/com/commonsware/todo/DisplayFragment.java)

Here, we get control when the user taps an action bar item. If that is our edit item, we get the ToDoModel that we are displaying from the TodoDisplayBinding and pass that to the editModel() implementation on the hosting activity, via the Contract interface. Finally, we need to actually display the EditFragment. Modify editModel() in MainActivity to run another FragmentTransaction: @Override public void editModel(ToDoModel model) {

231


EDITING AN ITEM getSupportFragmentManager().beginTransaction() .replace(android.R.id.content, EditFragment.newInstance(model)) .addToBackStack(null null) .commit(); } (from T18-Edit/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

If you run the sample app now, and you click on one of the to-do items, and then click on the “edit” action bar item, you will be taken to the empty EditFragment. If you press BACK when viewing the (empty) EditFragment, you will return to the DisplayFragment, and pressing BACK from there will return you to the list of to-do items.

Step #6: Creating an Empty Layout As was the case with DisplayFragment, to have EditFragment show the contents of a ToDoModel and allow editing, it helps to have a layout resource. Right-click over the res/layout/ directory and choose “New” > “Layout resource file” from the context menu. In the dialog that appears, fill in todo_edit as the “File name” and ensure that the “Root element” is set to android.support.constraint.ConstraintLayout (which should be the default value). Then, click “OK” to close the dialog and create the mostly-empty resource file.

Step #7: Setting Up Data Binding As with the roster rows and DisplayFragment, we are going to use data binding to populate the widgets. In the Text sub-tab of the todo_edit editor, modify the layout to look like this: <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> > <data> <variable name="model" type="com.commonsware.todo.ToDoModel" />

232


EDITING AN ITEM </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> > </android.support.constraint.ConstraintLayout> </layout>

This sets up a model variable, the same as we used in previous data binding cases.

Step #8: Adding the CheckBox As with the roster rows — but unlike the DisplayFragment layout — we should have a CheckBox to allow the user to toggle the completion status of the to-do item being edited. In the “Design” sub-tab of the layout editor for todo_edit, drag a CheckBox from the “Buttons” group in the Palette pane into the preview area. Use the grab handles to add constraints tying the CheckBox to the top and start sides of the ConstraintLayout:

Figure 150: Android Studio Layout Designer, Showing Added CheckBox In the Attributes pane, clear out the contents of the “text” attribute, as we just want a bare checkbox, without a caption. Also, give the widget an ID of isCompleted. Then, switch to the “Text” sub-tab and add an android:checked attribute with a binding expression, to check the CheckBox based on the completion status of the ToDoModel: android:checked="@{model.isCompleted()}" (from T18-Edit/ToDo/app/src/main/res/layout/todo_edit.xml)

At this point, the XML should resemble:

233


EDITING AN ITEM <?xml version="1.0" encoding="utf-8"?> <layout xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"> > <data> <variable name="model" type="com.commonsware.todo.ToDoModel" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> > <CheckBox android:id="@+id/isCompleted" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:checked="@{model.isCompleted()}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>

Step #9: Creating the Description Field The other two things that the user should be able to edit here are the description and the notes. They should not be able to edit the created-on date — that is the date on which the to-do item was created, and so it should not change after creation. For the description and the notes, for now, we will use EditText widgets, the basic fields of Android. Later on, we will swap those out for something a bit more complicated, but this will do to get us started. Switch back to the “Design” sub-tab in the layout editor. In the Palette pane, in the “Text” category, drag a “Plain Text” widget into the design area. Using the grab handles, set up constraints to: • Tie the top and end sides of the EditText to the top and end sides of the ConstraintLayout

• Connect the start side of the EditText to the end side of the CheckBox

234


EDITING AN ITEM

Figure 151: Android Studio Layout Designer, Showing Added EditText Then, change the “layout_width” attribute in the Attributes pane to match_constraint:

Figure 152: Android Studio Layout Designer, Showing Stretched EditText Then, give the EditText an ID of desc in the Attributes pane. If you look closely, you will see that our CheckBox is not very well aligned vertically with respect to the EditText:

Figure 153: Android Studio Layout Designer, Showing Vertical Alignment Problem Ideally, it would be vertically centered. To do that: • Remove the constraint tying the top of the CheckBox to the top of the ConstraintLayout, by clicking on the top grab handle • Drag a fresh constraint from the top of the CheckBox to the top of the EditText

• Create a similar constraint, from the bottom of the CheckBox to the bottom of the EditText

235


EDITING AN ITEM

Figure 154: Android Studio Layout Designer, Showing Aligned Widgets In the Attributes pane, the “Plain Text” widget that we dragged into the preview gave us an EditText set up with an “inputType” of textPersonName:

Figure 155: Android Studio Layout Designer, Attributes Pane, Showing “inputType” The android:inputType attribute provides hints to soft keyboards as to what we expect to use as input. For example, in languages where there is a distinction between uppercase and lowercase letters, textPersonName might trigger a switch to an uppercase keyboard for each portion of a name. In this case, we really want plain text, so click on textPersonName and choose text from the checklist, and uncheck textPersonName:

236


EDITING AN ITEM

Figure 156: Android Studio Layout Designer, Attributes Pane, Showing “inputType” Pop-Up An EditText has an android:hint attribute. This provides some text that will appear in the field in gray when there is no actual text entered by the user in the field. Once the user starts typing, the hint goes away. This is used to save space over having a separate label or caption for the field. With that in mind, click the “…” button for the “hint” attribute in the Attributes pane. Create a new string resource using the drop-down menu. Give the resource a name of desc and a value of Description. Then, click OK to define the string resource and apply it to the hint. Finally, switch to the “Text” sub-tab and replace the existing android:text attribute value with a binding expression: android:text="@{model.description()}" (from T18-Edit/ToDo/app/src/main/res/layout/todo_edit.xml)

At this point, the layout XML should resemble: <?xml version="1.0" encoding="utf-8"?> <layout xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"> >

237


EDITING AN ITEM <data> <variable name="model" type="com.commonsware.todo.ToDoModel" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> > <CheckBox android:id="@+id/isCompleted" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:checked="@{model.isCompleted()}" app:layout_constraintBottom_toBottomOf="@+id/desc" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/desc" /> <EditText android:id="@+id/desc" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:ems="10" android:hint="@string/desc" android:inputType="text" android:text="@{model.description()}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/checkBox" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>

Step #10: Adding the Notes Field The other widget is another EditText, this time for the notes.

238


EDITING AN ITEM Switch back to the “Design” sub-tab in the layout editor. In the Palette pane, in the “Text” category, drag a “Multiline Text” widget into the design area. Using the grab handles, set up constraints to: • Tie the bottom, start, and end sides of the EditText to the bottom, start, and end sides of the ConstraintLayout • Tie the top of the EditText to the bottom of the previous EditText

Figure 157: Android Studio Layout Designer, Showing Added EditText Then, set both the “layout_height” and “layout_weight” attributes to match_constraint:

239


EDITING AN ITEM

Figure 158: Android Studio Layout Designer, Showing Big EditText Give the widget an ID of notes. Click the “…” button for the “hint” attribute in the Attributes pane. Create a new string resource using the drop-down menu. Give the resource a name of notes and a value of Notes. Then, click OK to define the string resource and apply it to the hint. At this point, you may notice something odd in the preview:

240


EDITING AN ITEM

Figure 159: Android Studio Layout Designer, Showing Strange Hint Location Widgets that contain content — like an EditText — can have the concept of “gravity”. Gravity indicates where the content should go, if the content is smaller than the size of the widget itself. The EditText widget’s default gravity has the text be vertically centered. Ideally, the layout editor would handle this for us. Perhaps in the future it will. In the meantime, we need to fix this ourselves. Switch the Attributes pane to show all attributes, then scroll down to the “gravity” attribute. Click the triangle to show all of the available options, and choose “top” and “start”:

241


EDITING AN ITEM

Figure 160: Android Studio Layout Designer, Showing Gravity Options (Android Studio will also show “left” as checked, as that is tied to “start”) If you switch back to the “Design” sub-tab, you will now see our hint at a more typical location:

Figure 161: Android Studio Layout Designer, Showing Better Hint Location Finally, switch back to the “Text” sub-tab and add an android:text attribute with a binding expression: 242


EDITING AN ITEM android:text="@{model.notes()}" (from T18-Edit/ToDo/app/src/main/res/layout/todo_edit.xml)

At this point, the layout is complete and should resemble: <?xml version="1.0" encoding="utf-8"?> <layout xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"> > <data> <variable name="model" type="com.commonsware.todo.ToDoModel" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> > <CheckBox android:id="@+id/isCompleted" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:checked="@{model.isCompleted()}" app:layout_constraintBottom_toBottomOf="@+id/desc" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/desc" /> <EditText android:id="@+id/desc" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:ems="10" android:hint="@string/desc" android:inputType="text" android:text="@{model.description()}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/isCompleted" app:layout_constraintTop_toTopOf="parent" />

243


EDITING AN ITEM <EditText android:id="@+id/notes" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:ems="10" android:text="@{model.notes()}" android:gravity="top|start" android:hint="@string/notes" android:inputType="textMultiLine" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/desc" /> </android.support.constraint.ConstraintLayout> </layout> (from T18-Edit/ToDo/app/src/main/res/layout/todo_edit.xml)

Step #11: Populating the Layout Now, we can add the same sort of logic in EditFragment to bind the ToDoModel that we added to DisplayFragment. Add a field for a TodoEditBinding: private TodoEditBinding binding; (from T18-Edit/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

Then, add these methods: @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding=TodoEditBinding.inflate(getLayoutInflater(), container, false false); return binding.getRoot(); } @Override

244


EDITING AN ITEM public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super super.onViewCreated(view, savedInstanceState); binding.setModel(ToDoRepository.get().find(getModelId())); } (from T18-Edit/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

As before, we inflate our binding in onCreateView(), then bind our model in onViewCreated(). If you run the app, click on a to-do item to display it, then click on the â&#x20AC;&#x153;editâ&#x20AC;? action bar item, you will get a form for modifying the to-do item:

Figure 162: ToDo App, Showing EditFragment Note that EditText only word-wraps when set up for multiline. Otherwise, long text just scrolls off the end. This is perfectly normal. A bigger problem is that our changes are not being reflected anywhere. For that, we will need to update our models, and we will deal with that in the next tutorial.

245


EDITING AN ITEM

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • • • • • • • •

app/src/main/java/com/commonsware/todo/EditFragment.java app/src/main/res/drawable/ic_edit_black_24dp.xml app/src/main/res/menu/actions_display.xml app/src/main/java/com/commonsware/todo/MainActivity.java app/src/main/java/com/commonsware/todo/DisplayFragment.java app/src/main/res/layout/todo_edit.xml app/src/main/res/drawable/ic_check_circle_black_24dp.xml app/src/main/java/com/commonsware/todo/ToDoRepository.java

246


Saving an Item

Having the EditFragment is nice, but we are not saving the changes anywhere. As soon as we leave the fragment, the “edits” vanish. This is not ideal. So, in this tutorial, we will allow the user to save their changes, by clicking a suitable action bar item. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Step #1: Adding the Action Bar Item First, let’s set up the Save action bar item. Right-click over res/drawable/ in the project tree and choose “New” > “Vector Asset” from the context menu. This brings up the Vector Asset Wizard. There, click the “Icon” button and search for save:

247


SAVING AN ITEM

Figure 163: Android Studio Vector Asset Selector, Showing “save” Options Choose the “save” icon and click “OK” to close up the icon selector. Then, click “Next” and “Finish” to close up the wizard and set up our icon. Then, right-click over the res/menu/ directory and choose New > “Menu resource file” from the context menu. Fill in actions_edit.xml in the “New Menu Resource File” dialog, then click OK to create the file to open it in the menu editor. In the Palette, drag a “Menu Item” into the preview area. This will appear as an item in an overflow area:

248


SAVING AN ITEM

Figure 164: Android Studio Menu Editor, Showing Added MenuItem In the Attributes pane, fill in save for the “id”. Then, choose both “ifRoom” and “withText” for the “showAsAction” option. Next, click on the “…” button next to the “icon” field. This will bring up an drawable resource selector — click on ic_save_black_24dp in the list of drawables, then click OK to accept that choice of icon. Then, click the “…” button next to the “title” field. As before, this brings up a string resource selector. Click on “Add new resource” > “New string Value” in the dropdown towards the top. In the dialog, fill in menu_save as the resource name and “Save” as the resource value. Click OK to close the dialog, to complete our work on setting up the action bar item:

Figure 165: Android Studio Menu Editor, Showing Configured MenuItem 249


SAVING AN ITEM We also need to take steps to arrange to show this action bar item on EditFragment, as we did with DisplayFragment for the â&#x20AC;&#x153;editâ&#x20AC;? item. Add this onCreate() method to EditFragment, to indicate that this fragment wishes to participate in the action bar: @Override public void onCreate(@Nullable Bundle savedInstanceState) { super super.onCreate(savedInstanceState); setHasOptionsMenu(true true); } (from T19-Save/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

Next, add this onCreateOptionsMenu() method to EditFragment, to inflate our newly-created menu resource: @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.actions_edit, menu); super super.onCreateOptionsMenu(menu, inflater); } (from T19-Save/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

If you run the app and edit a to-do item, you should see the new action bar item on the EditFragment:

250


SAVING AN ITEM

Figure 166: ToDo App, EditFragment, with Save Action Bar Item

Step #2: Replacing the Item Now that we have the action bar item, we can get control when it is clicked and update our repository with a revised ToDoModel. Create this save() method on EditFragment: private void save() { ToDoModel newModel=binding.getModel().toBuilder() .description(binding.desc.getText().toString()) .notes(binding.notes.getText().toString()) .isCompleted(binding.isCompleted.isChecked()) .build(); ToDoRepository.get().replace(newModel); }

Here we: â&#x20AC;˘ Retrieve our current ToDoModel from the binding â&#x20AC;˘ Create a ToDoModel.Builder based on the contents of the current ToDoModel 251


SAVING AN ITEM • Update the Builder with the values from the widgets in our layout • Create a new ToDoModel frmo the Builder • Tell the ToDoRepository to replace the existing ToDoModel for this ID with this replacement Then, arrange to call this save() method when the user clicks on the “save” action bar item, by adding this onOptionsItemSelected() method to EditFragment: @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId()==R.id.save) { save(); return true true; } return super super.onOptionsItemSelected(item); } (from T19-Save/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

If you run the app, select some to-do item, make some change to that item, then click that action bar item… nothing seems to happen. But, if you then press BACK to return to the DisplayFragment, you will see the change that you made to the to-do item.

Step #3: Returning to the Display Fragment The “nothing seems to happen” bit from the preceding step is a problem. Usually, when the user clicks a “save” option in an app, not only does the data get saved, but the user is taken to some other portion of the app. In the case of EditFragment, we could send the user back to the DisplayFragment that they came from. To do this, we need to revert the FragmentTransaction that displayed the EditFragment in the first place. However, while an activity can destroy itself via finish(), there is nothing in the fragment API for a fragment to say “please get rid of me”. So, we need to pass control again to MainActivity, which can revert the transaction. With that in mind, add this finishEdit() method to MainActivity: public void finishEdit() { getSupportFragmentManager().popBackStack(); }

252


SAVING AN ITEM This simply “pops the back stack” — since we put the EditFragment on the back stack, not only will the BACK button revert the FragmentTransaction that displayed the EditFragment, but popBackStack() will as well. Following the contract pattern used in the other fragments, add this Contract interface to EditFragment: interface Contract { void finishEdit(); } (from T19-Save/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

Then, modify MainActivity to also implement the EditFragment.Contract interface: public class MainActivity extends FragmentActivity implements RosterListFragment.Contract, DisplayFragment.Contract, EditFragment.Contract { (from T19-Save/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

That will allow you to add the @Override annotation to finishEdit(): @Override public void finishEdit() { getSupportFragmentManager().popBackStack(); }

Then, add a call to finishEdit() in the save() method in EditFragment: private void save() { ToDoModel newModel=binding.getModel().toBuilder() .description(binding.desc.getText().toString()) .notes(binding.notes.getText().toString()) .isCompleted(binding.isCompleted.isChecked()) .build(); ToDoRepository.get().replace(newModel); ((Contract)getActivity()).finishEdit(); } (from T19-Save/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

If you run the sample app now, when you click “save” in the EditFragment, you go back to the DisplayFragment. 253


SAVING AN ITEM However, if you are using a device with a soft keyboard, that soft keyboard may still be visible after clicking “save”. This is annoying. But, with some trickery, we can fix it. Add this method to MainActivity: private void hideSoftInput() { if (getCurrentFocus()!=null null && getCurrentFocus().getWindowToken()!=null null) { ((InputMethodManager)getSystemService(INPUT_METHOD_SERVICE)) .hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0); } } (from T19-Save/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

This method, from this Stack Overflow answer, hides the soft keyboard (a.k.a., “input method editor”). This code is clunky but is unavoidable. Then, modify finishEdit() in MainActivity to call hideSoftInput(): @Override public void finishEdit() { hideSoftInput(); getSupportFragmentManager().popBackStack(); } (from T19-Save/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

Now, if you run the sample app and edit a to-do item, saving your changes both returns you to the DisplayFragment and hides the soft keyboard if needed.

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • • • •

app/src/main/res/drawable/ic_save_black_24dp.xml app/src/main/res/menu/actions_edit.xml app/src/main/java/com/commonsware/todo/EditFragment.java app/src/main/java/com/commonsware/todo/MainActivity.java

254


Adding and Deleting Items

Now, we can edit our to-do items. However, the app is still pretty limited, in that we can only have exactly three to-do items. While we can now change what appears in those to-do items, we cannot add or remove any. We really should fix that. So, in this tutorial, we will wrap up the “glassware” portion of the app, by getting rid of the fake starter data and giving the user the ability to add new to-do items and delete existing ones. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Step #1: Removing the Sample Data In ToDoRepository, remove the constructor, which is where we were setting up the fake data. If you now run the sample app, it runs, but we have no to-do items:

255


ADDING AND DELETING ITEMS

Figure 167: ToDo App, Showing Nothing

Step #2: Showing an Empty View Dumping the user onto an empty screen at the outset is rather unfriendly. A typical solution is to have an “empty view” that is displayed when there is nothing else to show. That “empty view” usually has a message that tells the user what to do first. We created the empty view back in an earlier tutorial, but we set its visibility to GONE. Let’s revert that change, so the empty view appears to the user. In onViewCreated() of RosterListFragment, remove the empty.setVisibility(View.GONE); line, leaving you with: @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { rv.setLayoutManager(new new LinearLayoutManager(getActivity())); DividerItemDecoration decoration=new new DividerItemDecoration(getActivity(), LinearLayoutManager.VERTICAL); rv.addItemDecoration(decoration);

256


ADDING AND DELETING ITEMS rv.setAdapter(new new RosterListAdapter(this this)); }

Now when you run the app, you will see… some placeholder text:

Figure 168: ToDo App, Showing Placeholder Empty Text We will replace that text with a better message shortly.

Step #3: Adding an Add Action Bar Item We need to add another action bar item, this one in the roster fragment, to allow the user to add a new to-do item. Right-click over res/drawable/ in the project tree and choose “New” > “Vector Asset” from the context menu. This brings up the Vector Asset Wizard. There, click the “Icon” button and search for add:

257


ADDING AND DELETING ITEMS

Figure 169: Android Studio Vector Asset Selector, Showing “add” Options Choose the “add” icon and click “OK” to close up the icon selector. Then, click “Next” and “Finish” to close up the wizard and set up our icon. While it feels like we keep adding action bar items, we have never added one directly to the RosterListFragment. All previous action bar items were added to the other fragments or to MainActivity. So, we need to set up a new menu resource and the corresponding Java code. Right-click over the res/menu/ directory and choose New > “Menu resource file” from the context menu. Fill in actions_roster.xml in the “New Menu Resource File” dialog, then click OK to create the file to open it in the menu editor. In the Palette, drag a “Menu Item” into the preview area. In the Attributes pane, fill in add for the “id”. Then, choose both “ifRoom” and “withText” for the “showAsAction” option. Next, click on the “…” button next to the “icon” field. This will bring up an drawable resource selector. Click on ic_add_black_24dp in the list of drawables, then click OK to accept that choice of icon. Then, click the “…” button next to the “title” field. As before, this brings up a string resource selector. Click on “Add new resource” > “New string Value” in the dropdown towards the top. In the dialog, fill in menu_add as the resource name and “Add” 258


ADDING AND DELETING ITEMS as the resource value. Click OK to close the dialog and complete the configuration of this action bar item:

Figure 170: Android Studio Menu Editor, Showing Configured MenuItem Add this onCreate() method to RosterListFragment, to indicate that this fragment wishes to participate in the action bar: @Override public void onCreate(@Nullable Bundle savedInstanceState) { super super.onCreate(savedInstanceState); setHasOptionsMenu(true true); } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

Next, add this onCreateOptionsMenu() method to RosterListFragment, to inflate our newly-created menu resource: @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.actions_roster, menu); super super.onCreateOptionsMenu(menu, inflater); } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

259


ADDING AND DELETING ITEMS Finally, open up the res/values/strings.xml resource file. You should find a string resource named msg_empty in there, with a value of placeholder text. Replace that value with Click the + icon to add a todo item!. Now, when you run the app, not only do you get the “add” action bar item, but the empty view text is more useful:

Figure 171: ToDo App, with Add Action Bar Item and Better Empty Text

Step #4: Launching the EditFragment for Adds Next, we need to add some logic to do some work when the user taps that “add” action bar item. When the user taps on a to-do item in the list, we call a showModel() method on the RosterListFragment.Contract interface, triggering MainActivity to show the DisplayFragment for that to-do item. We need a nearclone of that logic to kick off an EditFragment for the purposes of “editing” a new to-do item. Let’s start with the activity. Add this addModel() method to MainActivity:

260


ADDING AND DELETING ITEMS public void addModel() { editModel(null null); }

This just calls editModel() with a null ToDoModel, using null to indicate that we have no existing to-do item to edit and should therefore create a new item. We will need to fix up EditFragment to support this, and we will work on that in this step and the next one. Then, add addModel() to the Contract interface on RosterListFragment: interface Contract { void showModel(ToDoModel model); void addModel(); } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

This will allow you to add the @Override annotation to addModel() on MainActivity: @Override public void addModel() { editModel(null null); } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

Next, add this onOptionsItemSelected() implementation to RosterListFragment: @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId()==R.id.add) { ((Contract)getActivity()).addModel(); return true true; } return super super.onOptionsItemSelected(item); } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

This calls addModel() on the Contract when the user clicks the “add” action bar item. So now clicking the “add” action bar item will show the EditFragment.

261


ADDING AND DELETING ITEMS However, EditFragment has a problem: its getModelId() method assumes that there is an arguments Bundle: private String getModelId() { return getArguments().getString(ARG_ID); }

…but our newInstance() factory method skips the Bundle if the supplied ToDoModel is null: static EditFragment newInstance(ToDoModel model) { EditFragment result=new new EditFragment(); if (model!=null null) { Bundle args=new new Bundle(); args.putString(ARG_ID, model.id()); result.setArguments(args); } return result; } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

So, change getModelId() to return null if the arguments Bundle itself is null: private String getModelId() { return getArguments()==null null ? null : getArguments().getString(ARG_ID); } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

If you run the app and click the “add” action bar item, you should get an empty EditFragment form, showing our hints for the description and notes fields:

262


ADDING AND DELETING ITEMS

Figure 172: ToDo App, Showing Empty EditFragment However, clicking the “save” action bar item will cause problems, which we will fix in the next step.

Step #5: Adjusting Our Save Logic Our current save() method on EditFragment assumes that we bound a ToDoModel into our TodoEditBinding: private void save() { ToDoModel newModel=binding.getModel().toBuilder() .description(binding.desc.getText().toString()) .notes(binding.notes.getText().toString()) .isCompleted(binding.isCompleted.isChecked()) .build(); ToDoRepository.get().replace(newModel); ((Contract)getActivity()).finishEdit(); }

263


ADDING AND DELETING ITEMS That is no longer the case, so we need to handle that scenario. Plus, we will want to call our repository’s add() method — not its replace() method – when we add the new item. So, replace the current save() method on EditFragment with the following: private void save() { ToDoModel.Builder builder; if (binding.getModel()==null null) { builder=ToDoModel.creator(); } else { builder=binding.getModel().toBuilder(); } ToDoModel newModel=builder .description(binding.desc.getText().toString()) .notes(binding.notes.getText().toString()) .isCompleted(binding.isCompleted.isChecked()) .build(); if (binding.getModel()==null null) { ToDoRepository.get().add(newModel); } else { ToDoRepository.get().replace(newModel); } ((Contract)getActivity()).finishEdit(); }

If we have no model, we create a new ToDoModel.Builder via the creator() static method. This will build a brand-new to-do item. If we have a model, we create a Builder based on its current values as before. Then, we call the Builder methods and build() the ToDoModel, so that no matter what Builder we are using, it contains the data from the form. Then, we call add() or replace() on the repository, depending on whether we had an existing model or not. If you run the sample app, click the “add” action bar item, fill in the form, and click the “save” action bar item, you wind up seeing the list of to-do items… with the empty text still visible:

264


ADDING AND DELETING ITEMS

Figure 173: ToDo App, Showing Both an Item and the Empty Text

Step #6: Hiding the Empty View Showing the empty view with just one to-do item is not so bad. The problem is that when we get enough to-do items, we wind up with overlapping text:

265


ADDING AND DELETING ITEMS

Figure 174: ToDo App, Showing Items Overlapping the Empty Text Besides, the point of the empty view is to show it only when the list is empty. To make that happen, add these lines to the bottom of onViewCreated() on RosterListFragment: if (rv.getAdapter().getItemCount()>0) { empty.setVisibility(View.GONE); } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

This makes the entire method become: @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { rv.setLayoutManager(new new LinearLayoutManager(getActivity())); DividerItemDecoration decoration=new new DividerItemDecoration(getActivity(), LinearLayoutManager.VERTICAL); rv.addItemDecoration(decoration); rv.setAdapter(new new RosterListAdapter(this this));

266


ADDING AND DELETING ITEMS if (rv.getAdapter().getItemCount()>0) { empty.setVisibility(View.GONE); } } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

So, we check to see if the RecyclerView.Adapter has any items, and if it does, we set the empty view to be GONE. Now, if you run the app, you will see the empty view at the outset — when we have no items — but the empty view will go away once you start adding items.

Step #7: Adding a Delete Action Bar Item We have one more action bar item to create, this one to allow the user to delete an item. We will add that to the action bar on EditFragment, so the user can delete the item from there. Right-click over res/drawable/ in the project tree and choose “New” > “Vector Asset” from the context menu. This brings up the Vector Asset Wizard. There, click the “Icon” button and search for delete:

Figure 175: Android Studio Vector Asset Selector, Showing “delete” Options 267


ADDING AND DELETING ITEMS Choose the “delete” icon and click “OK” to close up the icon selector. Then, click “Next” and “Finish” to close up the wizard and set up our icon. Open res/menu/actions_edit.xml in the IDE. In the Design sub-tab, drag a second “Menu Item” into the preview area:

Figure 176: Android Studio Menu Editor, Showing Second MenuItem In the Attributes pane, fill in delete for the “id”. Then, choose both “ifRoom” and “withText” for the “showAsAction” option. Next, click on the “…” button next to the “icon” field. This will bring up an drawable resource selector. Click on ic_add_black_24dp in the list of drawables, then click OK to accept that choice of icon. Then, click the “…” button next to the “title” field. As before, this brings up a string resource selector. Click on “Add new resource” > “New string Value” in the drop-down towards the top. In the dialog, fill in menu_delete as the resource name and “Delete” as the resource value. Click OK to close the dialog, to complete the configuration of this action bar item:

268


ADDING AND DELETING ITEMS

Figure 177: Android Studio Menu Editor, Showing Configured Second MenuItem Now, when you run the app and you go to add a new to-do item, or later you edit an existing to-do item, you will see the “delete” action bar item:

Figure 178: ToDo App, Edit Fragment, Showing Two Action Bar Items The fact that there is a delete icon for an add operation is… disturbing. We will address that later in this tutorial. 269


ADDING AND DELETING ITEMS

Step #8: Deleting the Item Deleting the ToDoModel seems fairly straightforward: call delete() on the ToDoRepository, supplying the model to be deleted. So, add this delete() method to EditFragment: private void delete() { ToDoRepository.get().delete(binding.getModel()); ((Contract)getActivity()).finishEdit(); }

This deletes the current model in the binding, plus calls the same finishEdit() that we do from save() to tell the MainActivity that we are done editing. Then, add an else if clause to onOptionsItemSelected() on EditFragment to call delete() if the user taps the “delete” action bar item: @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId()==R.id.save) { save(); return true true; } else if (item.getItemId()==R.id.delete) { delete(); return true true; } return super super.onOptionsItemSelected(item); } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

If you run the sample app, add a to-do item, save that item, click on that item in the roster, click on the “edit” action bar item to edit it, and then click the “delete” action bar item to delete it… you crash. The stack trace will be something like this: java.lang.NullPointerException: Attempt to invoke virtual method 'java.util.Calendar com.commonsware.todo.ToDoModel.createdOn()' on a null object reference at com.commonsware.todo.DisplayFragment.onViewCreated(DisplayFragment.java:86)

270


ADDING AND DELETING ITEMS at at at at at at at at at at at at at at at at

android.support.v4.app.FragmentManagerImpl.moveToState(... android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(... android.support.v4.app.FragmentManagerImpl.moveToState(... android.support.v4.app.BackStackRecord.executePopOps(BackStackRecord.java:857) android.support.v4.app.FragmentManagerImpl.executeOps(... android.support.v4.app.FragmentManagerImpl.executeOpsTogether(... android.support.v4.app.FragmentManagerImpl.removeRedundantOperationsAnd... android.support.v4.app.FragmentManagerImpl.execPendingActions(... android.support.v4.app.FragmentManagerImpl$1.run(FragmentManager.java:700) android.os.Handler.handleCallback(Handler.java:739) android.os.Handler.dispatchMessage(Handler.java:95) android.os.Looper.loop(Looper.java:148) android.app.ActivityThread.main(ActivityThread.java:5551) java.lang.reflect.Method.invoke(Native Method) com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(... com.android.internal.os.ZygoteInit.main(ZygoteInit.java:620)

(some lines were truncated with ... to help it to fit on the page) To understand what is going on, letâ&#x20AC;&#x2122;s look at our finishEdit() implementation in MainActivity: @Override public void finishEdit() { hideSoftInput(); getSupportFragmentManager().popBackStack(); }

From a navigation standpoint, we are popping the last transaction off of the back stack. This works fine when we are adding a new to-do item. Our stack starts off like this:

Figure 179: Back Stack for Adding a To-Do Item Popping that last transaction returns us to the roster fragment. However, when we delete, our back stack looks like this:

271


ADDING AND DELETING ITEMS

Figure 180: Back Stack for Deleting a To-Do Item That is because we got to the EditFragment from the DisplayFragment. So, popping the last transaction returns us to the DisplayFragment. That is where we are crashing, as DisplayFragment tries to display the ToDoModel, but we deleted it. What we really want is to go back to the roster fragment in either situation. That can be done, with some modifications to MainActivity. The null that we have been passing into addToBackStack() is a name for this particular transaction. We can pass that name into popBackStack() to indicate that we want to pop multiple transactions, to either: â&#x20AC;˘ Pop all transactions that occurred after the named one, or â&#x20AC;˘ Pop all transactions that occurred after the named one, plus pop the named transaction as well We are not using addToBackStack() with the RosterListFragment. If we did, when the user pressed BACK from the list, they would get an empty activity, which is not what we want. However, since we are not using addToBackStack(), we cannot use popBackStack() to return to the roster directly. But, we can still leverage this named-transaction approach, just with more code. First, in MainActivity, define a BACK_STACK_SHOW constant: private static final String BACK_STACK_SHOW="showModel"; (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

Then, use that name in the addToBackStack() call in showModel(): @Override public void showModel(ToDoModel model) { getSupportFragmentManager().beginTransaction() .replace(android.R.id.content, DisplayFragment.newInstance(model)) .addToBackStack(BACK_STACK_SHOW) .commit(); }

272


ADDING AND DELETING ITEMS (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

Then, modify finishEdit() to take a boolean value, indicating whether or not we deleted the model, and use that value to decide between two different popBackStack() calls: @Override public void finishEdit(boolean deleted) { hideSoftInput(); if (deleted) { getSupportFragmentManager().popBackStack(BACK_STACK_SHOW, FragmentManager.POP_BACK_STACK_INCLUSIVE); } else { getSupportFragmentManager().popBackStack(); } } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/MainActivity.java)

If we deleted the model, we want to return to the roster, so we pop all transactions after BACK_STACK_SHOW and the BACK_STACK_SHOW transaction itself (the latter courtesy of POP_BACK_STACK_INCLUSIVE). If we did not delete the model, then we just pop a single transaction off of the back stack. This change violates the Contract in EditFragment, so modify it to accept a boolean on finishEdit(): interface Contract { void finishEdit(boolean deleted); } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

Pass false for that value in the call in save(): private void save() { ToDoModel.Builder builder; if (binding.getModel()==null null) { builder=ToDoModel.creator(); } else { builder=binding.getModel().toBuilder(); }

273


ADDING AND DELETING ITEMS ToDoModel newModel=builder .description(binding.desc.getText().toString()) .notes(binding.notes.getText().toString()) .isCompleted(binding.isCompleted.isChecked()) .build(); if (binding.getModel()==null null) { ToDoRepository.get().add(newModel); } else { ToDoRepository.get().replace(newModel); } ((Contract)getActivity()).finishEdit(false false); } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

Pass true for that value in the call in delete(): private void delete() { ToDoRepository.get().delete(binding.getModel()); ((Contract)getActivity()).finishEdit(true true); } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

Now, if you run the sample app, it works as expected… with just one issue, that we will address in the next step.

Step #9: Fixing the Delete-on-Add Problem Right now, when you edit an existing to-do item, the “delete” action bar item appears. It also appears when you are adding a new to-do item. This is unnecessary and may confuse the user. Fortunately, fixing this requires just one line of code: a call to setVisible() on the MenuItem corresponding to “delete”. Modify onCreateOptionsMenu() of EditFragment to look like: @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.actions_edit, menu);

274


ADDING AND DELETING ITEMS menu.findItem(R.id.delete).setVisible(binding.getModel()!=null null); super super.onCreateOptionsMenu(menu, inflater); } (from T20-Add/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

Here, we retrieve the delete MenuItem and call setVisibility() with true if we have a model, false otherwise. This has the desired effect: removing the “delete” item if we do not have anything to delete.

Step #10: Fix Our Tests Our instrumentation tests — in the RepoTests class — were written expecting the ToDoRepository to contain three fake to-do items. Our repository no longer has those, and so the tests will fail. The simplest solution: put those three fake to-do items into the repository for our tests. So, modify RepoTests to have setUp() add those three items: @Before public void setUp() { repo=new new ToDoRepository(); repo.add(ToDoModel.creator() .description("Buy a copy of _Exploring Android_") .notes("See https://wares.commonsware.com") .isCompleted(true true) .build()); repo.add(ToDoModel.creator() .description("Complete all of the tutorials") .build()); repo.add(ToDoModel.creator() .description("Write an app for somebody in my community") .notes("Talk to some people at non-profit organizations to see what they need!") .build()); }

(from T20-Add/ToDo/app/src/androidTest/java/com/commonsware/todo/RepoTests.java)

If you run the RepoTests again, you will see that they pass.

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: 275


ADDING AND DELETING ITEMS • • • • • • • • •

app/src/main/java/com/commonsware/todo/ToDoRepository.java app/src/main/java/com/commonsware/todo/RosterListFragment.java app/src/main/res/drawable/ic_add_black_24dp.xml app/src/main/res/menu/actions_roster.xml app/src/main/res/drawable/ic_delete_black_24dp.xml app/src/main/res/menu/actions_edit.xml app/src/main/java/com/commonsware/todo/EditFragment.java app/src/main/java/com/commonsware/todo/MainActivity.java app/src/androidTest/java/com/commonsware/todo/RepoTests.java

276


Phase Two: Architecture


Defining a View State

Our app works, albeit with obvious gaps. For example, all of our to-do items go away when the process terminates, as we are not saving them in a database or elsewhere. The problem is that any form of I/O — disk I/O for a database, network I/O for a server — takes time. Dealing with those slow operations gets tricky, and our current approach of just slapping up some code will start to become a problem. So, over the next set of tutorials, we will revise the app to adopt a more formal GUI architecture. Along the way, we will add support for storing the to-do items in a database, so that they outlive our process. The first step is to define a “view state”. This is an object that collects all of the data needed to render our UI, for some closely-coupled portion of that UI. In our case, we will define a view state that will be used by all three of our fragments, as they are all part of MainActivity and all work with the same data. However, our view state will not affect AboutActivity, as showing “about” information is not related to showing to-do items. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Read Me! The particular GUI architecture pattern that we will be adopting is called ModelView-Intent (MVI). Android’s Architecture Components has a few chapters on MVI. Two in particular to note: • Introducing Model-View-Intent 279


DEFINING A VIEW STATE • A Deep Dive Into MVI, which profiles an app that looks a lot like the app that we are building in these tutorials, almost as if these books are designed to work together Beyond that, you may wish to consider watching the following presentations: • Jake Wharton’s 2017 Devoxx US presentation – while Jake does not mention MVI, the architecture that he demonstrates is the Redux/MVI approach • Yousuf Haque’s droidcon NYC 2017 presentation on MVI • Benoît Quenaudon’s droidcon NYC 2017 presentation on MVI, and his associated sample app and blog post In particular, Jake Wharton’s presentation influenced much of what we will be doing here with respect to MVI.

Step #1: Creating a Stub AutoValue POJO Our ViewState class will be immutable — when something changes what we should be rendering, we will create a fresh ViewState. So, as we did with classes like ToDoModel, we will use AutoValue to make this class (mostly) immutable. Right-click over the com.commonsware.todo package in the java/ directory and choose “New” > “Java Class” from the context menu. This will bring up a dialog where we can define a new Java class. For the name, fill in ViewState, then click OK to create the class. Then, replace the generated contents with this: package com.commonsware.todo; import android.support.annotation.Nullable android.support.annotation.Nullable; import com.google.auto.value.AutoValue com.google.auto.value.AutoValue; import java.util.List java.util.List; @AutoValue public abstract class ViewState { public abstract List<ToDoModel> items(); @Nullable public abstract ToDoModel current(); static Builder builder() { return new AutoValue_ViewState.Builder(); }

280


DEFINING A VIEW STATE @AutoValue.Builder abstract static class Builder { abstract Builder items(List<ToDoModel> items); abstract Builder current(ToDoModel current); abstract ViewState build(); } }

Here, we define two properties on our ViewState: • items(), which is our list of ToDoModel objects to display in the list • current(), which is the ToDoModel to display in the display and edit fragments, and is set based on which one the user clicked on in the list We use the @Nullable annotation to tell AutoValue that it is OK for that property to be null. Otherwise, AutoValue will insist that we always supply a current model. Plus, we have our Builder class, with its build() method, and ViewState has a builder() to return instances of the Builder. Initially, AutoValue_ViewState will show up in red, as it will not be code-generated right away. Choose Build > “Make Module ‘app’” from the Android Studio main menu to compile the module and clear up that error.

Step #2: Creating Some Factories Now, let’s add two methods for handling two common cases of creating Builder instances: creating a Builder for an empty ViewState and creating a Builder to modify an existing ViewState. First, add this empty() implementation: static Builder empty() { return builder().items(Collections.unmodifiableList(new new ArrayList<>())); } (from T21-ViewState/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

We cannot have a null value for items(), as we have not marked that property as being @Nullable. Hence, an empty ViewState should have an empty items() list. Here, we create one using Collections.unmodifiableList(), to help with immutability.

281


DEFINING A VIEW STATE Then, add this toBuilder() implementation: Builder toBuilder() { return builder().items(items()).current(current()); } (from T21-ViewState/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

As we did with ToDoModel, our toBuilder() simply creates a Builder and fills in starting values based on the current instanceâ&#x20AC;&#x2122;s values.

What We Changed The bookâ&#x20AC;&#x2122;s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the one file that we changed: app/src/main/java/ com/commonsware/todo/ViewState.java

282


Stubbing a ViewModel

The Architecture Components library has a class named ViewModel. Its name evokes GUI architecture patterns like Model-View-ViewModel (MVVM). In reality, ViewModel and its supporting classes are there to help us with a key challenge in Android: configuration changes. A configuration change is any change in the device condition where Google thinks that we might want different resources. The most common configuration change is a change in the screen orientation, such as moving from portrait to landscape. We may want different layouts in this case, as our portrait layouts might be too tall for a landscape device, or our landscape layouts might be too wide for a portrait device. Android’s default behavior when a configuration change occurs is to destroy all visible activities and recreate them from scratch, so you can load the desired resources. However, we need some means to hold onto information during this change, so our new activity has access to the same data that our old activity did. There are many solutions to this problem — the chapter on resource sets and configurations in The Busy Coder’s Guide to Android Development covers many of the classic approaches. ViewModel is a newcomer, but it works fairly nicely, which is why we will use it here. So, in this tutorial, we will set up a basic ViewModel and make it available to our fragments. In the next tutorial, we will work on tying our ViewModel and that ViewState class together, so our ViewModel delivers updated ViewState instances to our UI for rendering. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

283


STUBBING A VIEWMODEL

Read Me! Coverage of ViewModel from the Architecture Components can be found in: • The Architecture Components documentation on ViewModel • The chapter on ViewModel in Android’s Architecture Components

Step #1: Adding the Dependency The support for ViewModel is part of the Architecture Components, but those components are divided into several dependencies. At present, we do not have the android.arch.lifecycle:extensions dependency that we need to get ViewModel and all of its related classes. So, open app/build.gradle. We already have two version constants defined, supportVer and autoValueVer. Add a third one: def archVer="1.1.1" (from T22-ViewModel/ToDo/app/build.gradle)

Then, in the dependencies closure, add another implementation line to pull in the desired dependency, using our version constant: implementation "android.arch.lifecycle:extensions:$archVer" (from T22-ViewModel/ToDo/app/build.gradle)

Android Studio should be asking you to “Sync Now” in a yellow banner — go ahead and click that link.

Step #2: Creating a Stub ViewModel There are two base classes that we can choose from: ViewModel and AndroidViewModel. The latter is for cases where we need a Context, that “god object” in Android that provides access to all sorts of Android-specific things. Long-term, we will need a Context for our database access, so we will use AndroidViewModel as our base class. So, once again, create a new Java class, by right-clicking over the com.commonsware.todo package in the java/ directory and choose “New” > “Java 284


STUBBING A VIEWMODEL Class” from the context menu. For the name, fill in RosterViewModel. For the superclass, start typing in AndroidViewModel, then choose android.arch.lifecycle.AndroidViewModel from the drop-down. Then click OK to create the class. When RosterViewModel appears in the editor, it will immediately have red undersquiggles, complaining about a missing constructor. With the text cursor in the error-marked code, press Alt-Enter ( Option-Return on macOS), and choose “Create constructor matching super” from the quick-fix menu. This should give you a class that looks like this: public class RosterViewModel extends AndroidViewModel { public RosterViewModel( @NonNull Application application) { super super(application); } }

Step #3: Lazy-Creating the ViewModel All three of our fragments need the same set of changes to use the RosterViewModel. In each, add a field named viewModel: private RosterViewModel viewModel;

And, in each, add a line to the bottom of onCreate() to populate that field: viewModel=ViewModelProviders.of(getActivity()).get(RosterViewModel.class);

ViewModelProviders

is the typical way that you get an instance of your ViewModel. It maintains a cache of ViewModel instances and either returns the existing ViewModel (if it was created earlier) or creates a new one (if the cache does not have one). That cache is keyed by the “scope” of the ViewModel. In our case, we are using the activity as the scope, by providing the result of getActivity() to the of() method on ViewModelProviders. This will give us the same RosterViewModel instance for all three of our fragments. Alternatively, we could have passed this to of(), and have gotten a separate RosterViewModel for each fragment. That would be useful in cases where the fragments had significantly different needs and could not share a ViewModel. In our case, these fragments are closely coupled, and so they can share a ViewModel. 285


STUBBING A VIEWMODEL Remember: add the field and the ViewModelProviders line to all three fragments: RosterListFragment, DisplayFragment, and EditFragment.

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • • • •

app/src/main/java/com/commonsware/todo/RosterViewModel.java app/src/main/java/com/commonsware/todo/DisplayFragment.java app/src/main/java/com/commonsware/todo/EditFragment.java app/src/main/java/com/commonsware/todo/RosterListFragment.java

286


Publishing LiveData

Part of the architecture that we are moving towards involves our fragments being delivered ViewState objects, representing what it is that they are supposed to be displaying to the user. This means that somehow, something is pushing ViewState objects over to the fragments. The RosterViewModel would seem to be a candidate, since our three fragments have access to that object. However, one of the tenets of view-models in GUI architectures is that while the UI (“view”) knows about the view-model, the reverse is not true. A view-model should not hold direct references to the views, which means that RosterViewModel should not have fields for RosterListFragment, DisplayFragment, and EditFragment. And, as a result, RosterViewModel has no good way to call a method on those fragments to say “hey, here is the latest ViewState!”. Instead, we turn to reactive programming. There are frameworks set up to serve as event-driven pipelines for data, ones where a stream objects can flow from producers to consumers. The most powerful of these available for Android developers is called RxJava, and we will be using RxJava coming up in later tutorials. However, while RxJava is available for Android developers, RxJava is a pure-Java library, and it knows little about Android. In particular, it knows nothing about configuration changes and how that affects our UI. The Architecture Components has its own reactive programming framework: LiveData. This is much smaller than RxJava, and it is less capable as a result. However, it is aware of configuration changes, which makes it useful for us when we want to deliver events to activities and fragments. LiveData can be used directly or as a bridge from RxJava into our activities and fragments.

287


PUBLISHING LIVEDATA So, in this tutorial, we will set up LiveData in our RosterViewModel to deliver ViewState objects to our fragments. Right now, we will do that “by hand”, while we continue building up our GUI architecture. Later, we will tie LiveData to RxJava, as RxJava will be handling our event flows through the rest of the app. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Read Me! Coverage of LiveData from the Architecture Components can be found in: • The Architecture Components documentation on LiveData • The chapter on LiveData in Android’s Architecture Components

Step #1: Holding a MutableLiveData LiveData

is an abstract class. We cannot directly create one; instead, we need to use some concrete class, or get a LiveData (of some form) from somebody else. For now, we will use a MutableLiveData, which is a fairly simple LiveData subclass. So, add this field to RosterViewModel: private final MutableLiveData<ViewState> states=new new MutableLiveData<>(); (from T23-LiveData/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

The name MutableLiveData comes from the fact that we, as the holder of the MutableLiveData, can feed it data to be published to whatever subscribes to it. The LiveData base class, on the other hand, has a public API designed solely for those subscribers. Therefore, to hide the implementation detail from our fragments, we should make the states field available via a method that returns a LiveData, not a MutableLiveData. In this case, we do not want fragments attempting to publish data, just consume it. So, add this method to RosterViewModel: public LiveData<ViewState> stateStream() { return states; }

288


PUBLISHING LIVEDATA (from T23-LiveData/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

Step #2: Publishing a ViewState Now, we need to have RosterViewModel be able to publish ViewState objects, or at least the first one. For the moment, we will have RosterViewModel do what RosterListAdapter does: go to the ToDoRepository and ask it for all() the items. We can then use that to build the ViewState. To that end, modify the RosterViewModel constructor to look like this: public RosterViewModel(@NonNull Application application) { super super(application); ViewState initial=ViewState.builder().items(ToDoRepository.get().all()).build(); states.postValue(initial); }

(from T23-LiveData/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

So, we build() a ViewState using its builder() and all() the items(). Then, we use postValue() to update our MutableLiveData with the newly-created ViewState. postValue() is how something outside of the MutableLiveData publishes new objects onto the live stream.

Step #3: Rendering a ViewState So, if our RosterListFragment can get this ViewState, we no longer need the RosterListAdapter to be working with the ToDoRepository. Instead of pulling data out of the repository, we have the RosterViewModel push data, in the form of ViewState objects and the LiveData stream. First, in RosterListAdapter, make the models field no longer final, as we need to update it after the constructor is invoked. Then, remove the line from the RosterListAdapter constructor that filled in the models from the repo, leaving you with: RosterListAdapter(RosterListFragment host) { this this.repo=ToDoRepository.get();

289


PUBLISHING LIVEDATA this this.host=host; } (from T23-LiveData/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

Add a new setState() method in RosterListAdapter, which will fill in the models field from a viewState: void setState(ViewState state) { models=state.items(); notifyDataSetChanged(); } (from T23-LiveData/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

The notifyDataSetChanged() call will alert the RecyclerView that we changed the entire data model associated with this adapter, and so it should re-draw its contents. We also need to handle the case where getItemCount() on our RosterListAdapter is called before we have received a ViewState. Right now, we blindly call size() on our models field, and that will be null if we have not received a ViewState yet. So, modify getItemCount() on RosterListAdapter to be: @Override public int getItemCount() { return models==null null ? 0 : models.size(); } (from T23-LiveData/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

Our RosterListFragment will need to call that setState() method at some point. Right now, we are not holding onto the RosterListAdapter ourselves â&#x20AC;&#x201D; we are just handing it to the RecyclerView. It will be more convenient if we hold onto it, so add an adapter field to RosterListFragment: private RosterListAdapter adapter; (from T23-LiveData/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

Then, add a new render() method that will accept a ViewState and pass it along to the adapter: public void render(ViewState state) { adapter.setState(state); if (rv.getAdapter().getItemCount()>0) {

290


PUBLISHING LIVEDATA empty.setVisibility(View.GONE); } } (from T23-LiveData/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

We also have the “hide the empty view when we have items” logic here now, as we only know if we have items after we hand the ViewState over to the adapter. Otherwise, the adapter does not know if it has any items. Finally, modify onViewCreated() on RosterListFragment to fill in the adapter field, in addition to handing that adapter over to the RecyclerView, while also removing that “hide the empty view when we have items” code that moved to render(): @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { rv.setLayoutManager(new new LinearLayoutManager(getActivity())); DividerItemDecoration decoration=new new DividerItemDecoration(getActivity(), LinearLayoutManager.VERTICAL); rv.addItemDecoration(decoration); adapter=new new RosterListAdapter(this this); rv.setAdapter(adapter); }

Step #4: Observing the Stream The last piece of glue code is to tie our render() method to the ViewState coming from the stateStream() from our RosterViewModel. Fortunately, that requires just one line of code, added to the end of onViewCreated(): viewModel.stateStream().observe(this this, this this::render); (from T23-LiveData/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

Here, we call observe() on the LiveData, saying that we want to find out about any ViewState objects published by that LiveData, including the last one published before we called observe(). We want the ViewState to be handed over to the render() method, via a Java 8 method reference (this::render). And, we want to find out about these state changes as long as our RosterListFragment is around, which we indicate by passing this as the first parameter to observe().

291


PUBLISHING LIVEDATA

Step #5: Pondering What We Are Missing You can run the app now… though it is rather broken at this point. The fundamental problem is that we only ever publish one ViewState, reflecting the initial state of the repository. Since we are not saving our items to disk, this means our list is always empty. Even after we add a new item via the EditFragment, while we update the repository, we are not updating the view state, and so our RosterListFragment remains oblivious to the changes. Clearly, we are going to need to fix this, which we will do over the course of the next few tutorials, as we complete the MVI architecture implementation. There are also some leaks in our abstractions: • We did not change DisplayFragment and EditFragment to use the ViewState, so they are still working directly with the repository • RosterListAdapter still works with the Repository in the replace() method Those too will be addressed over the next few tutorials.

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • app/src/main/java/com/commonsware/todo/RosterViewModel.java • app/src/main/java/com/commonsware/todo/RosterListAdapter.java • app/src/main/java/com/commonsware/todo/RosterListFragment.java

292


Adding Actions

In an earlier tutorial, we saw the name “Model-View-Intent” used for the GUI architecture that we will be employing. Unfortunately, this name does not work all that well in Android development, considering that we have existing Java classes named View and Intent that we use quite a bit. What generally falls under the “intent” name is what we will call an “action”. Actions are major app events triggered by the UI, such as: • The UI initially loading, in the form of the user launching our activity • The user tapping on an item in the list • The user adding a new item, editing an existing item, or deleting an existing item • And so on So, the overall flow that we are setting up involves: • Raising an action from our UI • (some magic happens) • Our UI gets new a ViewState, delivered via LiveData, representing the new information to display to the user Of course, we will be filling in the details of the “some magic happens” bit as we move along. But, for now, we will focus on crafting an Action class representing those actions that the UI can raise. As you will see, we actually wind up with a family of actions, for different event types, each containing the data needed to deal with that event (e.g., the particular item to be deleted).

293


ADDING ACTIONS This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Step #1: Creating the Base Action Class So, once again, create a new Java class, by right-clicking over the com.commonsware.todo package in the java/ directory and choose “New” > “Java Class” from the context menu. For the name, fill in Action. Mark it as abstract, then click OK to create the class, giving you: package com.commonsware.todo; public abstract class Action { }

Step #2: Defining Specific Actions Few places in the app will care about the specific type of action that they encounter. Anything that just passes actions from one place to another can just deal with some abstract “action”. There are two common ways of going about this: 1. Have a single Action class that represents all actions. This would use an enum or something to indicate the specific type of action that the Action represents (e.g., an Action.Type of LOAD or SHOW or ADD). The Action class would then have fields representing a superset of all possible pieces of data that all actions might need. 2. Have an Action interface or abstract class, for which concrete implementations are created for specific action types. Those concrete classes can then have the fields unique to their type. This is a bit more verbose, particularly in Java (compared to Kotlin). However, it does a better job of information hiding, and we do not wind up with lots of null fields representing things that are unused by a particular type of action. Our Action class from the preceding step represents the second approach. For concrete implementations, we could create dedicated Java classes that extend Action, such as EditAction and DeleteAction. Another approach — the one that 294


ADDING ACTIONS we will take here â&#x20AC;&#x201D; is to use static classes, so we have Action.Edit and Action.Delete and such. Each of those classes will use AutoValue, so that our action data is immutable. With that in mind, replace the code-generated Action implementation with the following: package com.commonsware.todo; import com.google.auto.value.AutoValue com.google.auto.value.AutoValue; import java.util.List java.util.List; public abstract class Action { @AutoValue public static abstract class Add extends Action { public abstract ToDoModel model(); } @AutoValue public static abstract class Edit extends Action { public abstract ToDoModel model(); } @AutoValue public static abstract class Delete extends Action { public abstract ToDoModel model(); } @AutoValue static abstract class Show extends Action { public abstract ToDoModel current(); } public static class Load extends Action { } }

Four of our actions have a single property: a ToDoModel representing the model that we are adding, editing, deleting, or showing. Only Action.Load does not need the model. However, we will be changing these over time:

295


ADDING ACTIONS â&#x20AC;˘ Eventually, Action.Delete will take a List of models. In a future tutorial, we will allow the user to select multiple items in the RosterListFragment, and then allow the user to delete all of them at once. At that point, we will need to revise Action.Delete to support that scenario. â&#x20AC;˘ That multiple-selection feature will require a few new actions, representing select and unselect events.

Step #3: Building Factory Methods Since the AutoValue-generated classes (and their constructors) have awkward names like AutoValue_Action_Add, we can simplify the creation of these actions by implementing a few factory methods. So, add these to Action itself (not to any of the subclasses): public static Action add(ToDoModel model) { return new AutoValue_Action_Add(model); } public static Action edit(ToDoModel model) { return new AutoValue_Action_Edit(model); } public static Action delete(ToDoModel model) { return new AutoValue_Action_Delete(model); } public static Action show(ToDoModel model) { return new AutoValue_Action_Show(model); } public static Action load() { return new Action.Load(); } (from T24-Actions/ToDo/app/src/main/java/com/commonsware/todo/Action.java)

So, now, to create an Action.Add instance, we just call Action.add(), supplying the ToDoModel representing the item to be added.

296


ADDING ACTIONS

What We Changed The bookâ&#x20AC;&#x2122;s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the one file that we changed: app/src/main/java/ com/commonsware/todo/Action.java

297


Creating a Controller

Let’s take a closer look at the data flow that we are trying to implement here, as part of our Model-View-Intent architecture:

Figure 181: MVI Flow Diagram The terms used in the diagram are from the architecture pattern. Sometimes, those do not reflect the actual classes that are serving in those roles: • the “View” is not a View, but rather our fragments • the “Reducer” is our RosterViewModel, named to be aligned with the Architecture Components • the “Model” is a combination of our model objects (ToDoModel) and their repository (ToDoRepository) We have only two more nouns to implement. Chief among those is the “Controller”, which is responsible for responding to actions published by the “View” and, among

299


CREATING A CONTROLLER other things, updating our “Model” in response. Since we have no particular ties to other names for this class, we can call our “Controller” Controller. We need to have the Controller respond to actions published by the fragments. The way that we will do that pulls in another popular library: RxJava. RxJava ushered in the era of reactive programming in Android, despite it being a pure Java library, not tied directly to Android at all. We will have our fragments publish actions using an RxJava Observable type, which represents a stream of events. The Controller will subscribe to that stream on a background thread and process those events. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Read Me! RxJava has a GitHub repository that has a lot of documentation. However, bear in mind that there are two major generations of RxJava: RxJava 1.x and RxJava 2.x. We are using the newer 2.x generation. The RxJava GitHub repository sometimes blurs the line between these generations, so be careful. Some introductory material on RxJava can be found in a chapter in The Busy Coder’s Guide to Android Development. Also, a lot has been written about RxJava and its use in Android. However, due to the aforementioned pair of RxJava generations, focus on material written in 2017 onwards. 2016 material that specifically mentions RxJava 2 is also fine. Older material will refer to RxJava 1, and while the concepts are the same, any specific instructions will be incorrect, due to changes in the way that RxJava works.

Step #1: Adding a Stub Class It’s time to add another Java class! How exciting! [Narrator: It was not exciting.] Right-click over the com.commonsware.todo package in the java/ directory and choose “New” > “Java Class” from the context menu. For the name, fill in Controller. Then click OK to create the class.

300


CREATING A CONTROLLER

Step #2: Accessing the Repository We have been moving around the part of the code that works with the ToDoRepository for a while now. Its final home â&#x20AC;&#x201D; at least with respect to these tutorials â&#x20AC;&#x201C; is in this Controller class. So, add a private final field to Controller, named toDoRepo, that initializes itself with the ToDoRepository, via get(): package com.commonsware.todo; public class Controller { private final ToDoRepository toDoRepo=ToDoRepository.get(); }

Step #3: Depending Upon RxJava We are going to start using RxJava, and to do that, we will need the RxJava artifact as a dependency. So, in app/build.gradle, add this line to the dependencies closure: implementation "io.reactivex.rxjava2:rxjava:2.1.7" (from T25-Controller/ToDo/app/build.gradle)

The overall dependencies closure, and related constants, should now look like: def supportVer="27.1.1" def autoValueVer="1.5.1" def archVer="1.1.1" dependencies { implementation "com.android.support:recyclerview-v7:$supportVer" implementation "com.android.support:support-fragment:$supportVer" implementation "android.arch.lifecycle:extensions:$archVer" implementation 'com.android.support.constraint:constraint-layout:1.1.0' implementation "io.reactivex.rxjava2:rxjava:2.1.7" compileOnly "com.google.auto.value:auto-value:$autoValueVer" annotationProcessor "com.google.auto.value:auto-value:$autoValueVer" testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }

(from T25-Controller/ToDo/app/build.gradle)

301


CREATING A CONTROLLER

Step #4: Subscribing to Actions We will need some code to process our actions. So, add this stub method to Controller: private void processImpl(Action action) { }

The way that we will get our actions over to that method will be through an RxJava Observable, one that our fragments will use to publish the actions that we can then consume. To that end, add this method to Controller: public void subscribeToActions(Observable<Action> actionStream) { actionStream .observeOn(Schedulers.io()) .subscribe(this this::processImpl); } (from T25-Controller/ToDo/app/src/main/java/com/commonsware/todo/Controller.java)

An RxJava Observable publishes a stream of whatever type you declare — in this case, this is an Observable of Action. We are handed such an Observable, and we set up a very short “RxJava chain”. The RxJava API is designed for you to chain a series of calls — reminiscent of a builder-style API — to describe what you want to have happen with the objects published on that stream. In this case, we make two calls: • observeOn() says what thread we want to use to receive the objects. In this case, we use a thread provided to use by RxJava itself, called Schedulers.io(), that is designed for background I/O work. Since (eventually) our items will be saved to a database, we should process those events on a background thread, so our UI is not frozen while we perform that I/O. • subscribe() says where and how we want to receive the objects themselves that are published to this stream. In this case, we use a Java 8 method reference to point to the processImpl() stub method. At the moment, nothing is calling subscribeToActions(), but we will address that later in this tutorial.

302


CREATING A CONTROLLER

Step #5: Handling Add, Edit, and Delete Actions Now, we can start processing these actions, updating the repository as needed. Add these three methods to Controller: private void add(ToDoModel model) { toDoRepo.add(model); } private void modify(ToDoModel model) { toDoRepo.replace(model); } private void delete(ToDoModel toDelete) { toDoRepo.delete(toDelete); } (from T25-Controller/ToDo/app/src/main/java/com/commonsware/todo/Controller.java)

These simply call through to the corresponding method on the ToDoRepository. In a future tutorial, though, we will add more logic to these methods. Then, modify processImpl() to call through to those methods: private void processImpl(Action action) { if (action instanceof Action.Add) { add(((Action.Add)action).model()); } else if (action instanceof Action.Edit) { modify(((Action.Edit)action).model()); } else if (action instanceof Action.Delete) { delete(((Action.Delete)action).model()); } } (from T25-Controller/ToDo/app/src/main/java/com/commonsware/todo/Controller.java)

Here, we detect the type of the action, then â&#x20AC;&#x153;unwrapâ&#x20AC;? the action and call the method that operates on the individual pieces of data held by the action itself. You will note that we are only handling three of our five actions here. We will address Action.Show and Action.Load later in the book.

303


CREATING A CONTROLLER

Step #6: Connecting to the Controller Of course, right now, nothing is using this Controller, making all of this work somewhat theoretical. So, letâ&#x20AC;&#x2122;s start addressing that, working our way back towards our fragments. The three fragments need to get actions over to the Controller, and we want to use the RxJava Observable to make it easy to have the Controller process those actions on a background thread. We could have three such Observable objects, one per fragment, if we wanted. Another approach is to use RosterViewModel in its viewmodel mode, and have it mediate communications to our Controller, including having the Observable that we need. Then, the fragments can just work with the RosterViewModel, and they already have access to it. To that end, add a PublishSubject field to RosterViewModel, named actionSubject: private final PublishSubject<Action> actionSubject= PublishSubject.create(); (from T25-Controller/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

A Subject in RxJava is an object that can serve as a source of events. We can hold a Subject, feed it Action objects, and our Controller can find out about those objects as they are published. Next, add these lines to the end of the RosterViewModel constructor: Controller controller=new new Controller(); controller.subscribeToActions(actionSubject); (from T25-Controller/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

Our RosterViewModel only communicates with the Controller by way of the actionSubject. Hence, we do not need a field for the Controller. All we need to do is get our hands on one, then have it subscribeToActions(), to have the Controller be connected to our stream of Action objects. Then, add this method to RosterViewModel:

304


CREATING A CONTROLLER public void process(Action action) { actionSubject.onNext(action); } (from T25-Controller/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

Our fragments will use this process() method to push an Action over to the RosterViewModel. onNext() is how you emit another object onto the stream of the Subject. So, when a fragment calls process(), RosterViewModel emits the Action on the actionSubject, which passes the Action to the Controller, which triggers a processImpl() call, which in turn updates the repository. Now all we need to do is tie UI events to this process() method, for relevant actions.

Step #7: Publishing List Actions Right now, RosterListAdapter is responsible for updating the repository when the user clicks the CheckBox for an item. Now, we want to publish an Action.Edit, to have the Controller make this modification. That means we no longer need access to the ToDoRepository in RosterListAdapter. So, remove the repo field from RosterListAdapter, along with the statement in the constructor that initialized it. Then, in the replace() method, have it call a replace() method on the host (RosterListFragment) instead of on the now nonexistent repo: public void replace(ToDoModel model, boolean isChecked) { host.replace(model.toBuilder().isCompleted(isChecked).build()); } (from T25-Controller/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

This will fail to compile, as there is no such method. So, on RosterListFragment, add this replace() implementation: void replace(ToDoModel model) { viewModel.process(Action.edit(model)); } (from T25-Controller/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

Now, a check on the CheckBox will trigger the Controller to update the repository. 305


CREATING A CONTROLLER

Step #8: Publishing Edit Actions We need to make similar modifications to EditFragment, replacing its direct use of ToDoRepository with actions. We do this in two methods: save() and delete(). delete()

is the simple one. Replace it with:

private void delete() { viewModel.process(Action.delete(binding.getModel())); ((Contract)getActivity()).finishEdit(true true); } (from T25-Controller/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

Here, we replace the ToDoRepository reference with an action. Similarly, in save(), modify the second if block to use actions instead of calls to add() and replace() on the repository: if (binding.getModel()==null null) { viewModel.process(Action.add(newModel)); } else { viewModel.process(Action.edit(newModel)); } (from T25-Controller/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

There is still one reference to ToDoRepository, in onViewCreated(). We will revise this later in the book.

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • • • • • •

app/src/main/java/com/commonsware/todo/Controller.java app/build.gradle app/src/main/java/com/commonsware/todo/RosterViewModel.java app/src/main/java/com/commonsware/todo/RosterListAdapter.java app/src/main/java/com/commonsware/todo/RosterListFragment.java app/src/main/java/com/commonsware/todo/EditFragment.java

306


Defining Results

The final leg of the MVI flow is to have our Controller let other parties – notably, the RosterViewModel — know about changes that were made to our data. That will allow the RosterViewModel to fill the “Reducer” role from the MVI diagram, delivering a fresh ViewState to our “View”:

Figure 182: MVI Flow Diagram. Again. In this tutorial, we will just set up a Result class. In the next tutorial, we will have the Controller publish results, have the RosterViewModel subscribe to find out about those results, and deliver new ViewState objects based upon those results. This tutorial is going to feel a lot like the tutorial where we defined the Action class. In this app, actions and results have a near 1:1 mapping. That is not always the case, though. For example, we could have had a dedicated Checked action, for when the user checks the CheckBox for an item. The Controller would handle creating the revised ToDoModel and updating the repository. However, the RosterViewModel does not care whether a ToDoModel changed because of a CheckBox in the list, the 307


DEFINING RESULTS EditFragment,

or something else. So, we would have the same result for two separate

actions. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Step #1: Creating the Base Result Class One more time, create a new Java class, by right-clicking over the com.commonsware.todo package in the java/ directory and choose “New” > “Java Class” from the context menu. For the name, fill in Result. Mark it as abstract, then click OK to create the class, giving you: package com.commonsware.todo; public abstract class Result { }

Step #2: Defining Specific Results We are going to use the same pattern for Result as we did with Action: have an abstract base class with a number of AutoValue-defined subclasses. Therefore, some of our app can just deal with Result, while other portions of the app can deal with the specific subclasses. package com.commonsware.todo; import com.google.auto.value.AutoValue com.google.auto.value.AutoValue; import java.util.List java.util.List; public abstract class Result { @AutoValue public static abstract class Added extends Result { public abstract ToDoModel model(); } @AutoValue public static abstract class Modified extends Result { public abstract ToDoModel model(); } @AutoValue

308


DEFINING RESULTS public static abstract class Deleted extends Result { public abstract ToDoModel model(); } @AutoValue static abstract class Showed extends Result { public abstract ToDoModel current(); } @AutoValue public static abstract class Loaded extends Result { public abstract List<ToDoModel> models(); } }

This sets up five Result subclasses that mirror the five Action subclasses. The biggest difference â&#x20AC;&#x201D; besides the past tense on the class names â&#x20AC;&#x201D; is that Loaded now has the results of the data load: a List of our model objects. In comparison, Action.Loaded has no properties.

Step #3: Building Factory Methods Finally, add corresponding factory methods to Result: public static Result added(ToDoModel model) { return new AutoValue_Result_Added(model); } public static Result modified(ToDoModel model) { return new AutoValue_Result_Modified(model); } static Result deleted(ToDoModel model) { return new AutoValue_Result_Deleted(model); } static Result showed(ToDoModel current) { return new AutoValue_Result_Showed(current); } static Result loaded(List<ToDoModel> models) { return new AutoValue_Result_Loaded(Collections.unmodifiableList(models)); } (from T26-Results/ToDo/app/src/main/java/com/commonsware/todo/Result.java)

309


DEFINING RESULTS These just wrap the AutoValue-generated constructors on the AutoValue-generated classes, to simplify creating instances of them.

What We Changed The bookâ&#x20AC;&#x2122;s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the one file that we changed: app/src/main/java/ com/commonsware/todo/Result.java

310


Completing the MVI Flow

We have all of the pieces necessary to build the Model-View-Intent (MVI) flow that we want. Now, we just need to make the final connections, getting our app back to the working state it was in before we started in on this round of changes. This also sets us up for saving our items to a database in upcoming tutorials. This is a continuation of the work we did in the previous tutorial. The bookâ&#x20AC;&#x2122;s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Step #1: Publishing Results Our Controller needs to emit Result objects when it is done processing the Action that triggers those results. We can use RxJava for this as well, so that objects needing those results can subscribe to them, control what thread those results are delivered upon, etc. To that end, add this resultSubject field to Controller: private final PublishSubject<Result> resultSubject= PublishSubject.create(); (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/Controller.java)

This sets up another RxJava Subject, akin to the PublishSubject that we used in our RosterViewModel for publishing actions. To allow subscribers to have access to that resultSubject, add this method to Controller:

311


COMPLETING THE MVI FLOW public Observable<Result> resultStream() { return resultSubject; } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/Controller.java)

Finally, we need to call onNext() on that resultSubject as part of processing each of our actions. So, modify the add(), modify(), and delete() methods to create and emit the associated Result objects: private void add(ToDoModel model) { toDoRepo.add(model); resultSubject.onNext(Result.added(model)); } private void modify(ToDoModel model) { toDoRepo.replace(model); resultSubject.onNext(Result.modified(model)); } private void delete(ToDoModel toDelete) { toDoRepo.delete(toDelete); resultSubject.onNext(Result.deleted(toDelete)); } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/Controller.java)

Step #2: Mutating the ViewState Given a Result and an existing ViewState, we need to publish a new and improved ViewState to the UI layer, reflecting the changes dictated by the Result. To that end, letâ&#x20AC;&#x2122;s add methods to ViewState that can create a replacement ViewState for common scenarios. Recall that ViewState is immutable; we cannot just change the existing ViewState, but instead we need to create a new ViewState reflecting the changes. To that end, implement this add() method on ViewState: ViewState add(ToDoModel model) { List<ToDoModel> models=new new ArrayList<>(items()); models.add(model); return toBuilder() .items(Collections.unmodifiableList(models))

312


COMPLETING THE MVI FLOW .current(model) .build(); }

Here, we: • Get the existing list of model objects in the current ViewState • Add a new model to that list • Create a new ViewState with the revised list (or, in reality, an unmodifiable list that wraps the revised list) However, there is a problem: we are just appending the new model object to the end of our list. This will mean that the to-do items will appear in the order that they were added. While that’s not an unreasonable choice, it would be better if we had them in a more controlled and useful sequence, such as appearing in alphabetical order by description. In Java, we can sort a list by using Collections.sort(), which either needs that the objects in the list are Comparable or that we supply a Comparator that can perform the comparisons. Let’s take that latter approach. Add this static field on ToDoModel: static final Comparator<ToDoModel> SORT_BY_DESC= (one, two) -> (one.description().compareTo(two.description())); (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/ToDoModel.java)

This creates a Comparator, via a Java 8 lambda expression, that compares two ToDoModel objects based on their description() values. Next, add this sort() method on ViewState: private void sort(List<ToDoModel> models) { Collections.sort(models, ToDoModel.SORT_BY_DESC); } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

This just sorts a list of models given that Comparator. Now, add in a call to sort() in the add() method that we just created: ViewState add(ToDoModel model) { List<ToDoModel> models=new new ArrayList<>(items());

313


COMPLETING THE MVI FLOW models.add(model); sort(models); return toBuilder() .items(Collections.unmodifiableList(models)) .current(model) .build(); } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

We will create equivalent methods for modifying an item and removing an item in later steps of this tutorial.

Step #3: Reducing the Results We need to make use of this add() method, to take a Result.Added event and generate the new ViewState based on that event. To that end, add this method to RosterViewModel: private ViewState foldResultIntoState(@NonNull ViewState state, @NonNull Result result) throws Exception { if (result instanceof Result.Added) { return state.add(((Result.Added)result).model()); } else { throw new IllegalStateException("Unexpected result type: "+result.toString()); } }

Here, we take the current ViewState and a Result and generate the new ViewState based upon that Result. Right now, we only handle the Result.Added scenario, but we will get to the others shortly.

Step #4: Publishing the ViewStates We need to have RosterViewModel subscribe to the stream of Result objects from the Controller, get a new ViewState using our new foldResultIntoState() method, and publish the new ViewState to the UI via a LiveData object. This will require us to replace some of our existing LiveData bits with new ones that can build us a pipeline from the RxJava Subject that the Controller offers and the LiveData that the UI uses. 314


COMPLETING THE MVI FLOW First, though, we need another dependency, as the code that serves as an RxJava-LiveData bridge comes from another piece of the Architecture Components. In your app/build.gradle file, add this line to the dependencies closure: implementation "android.arch.lifecycle:reactivestreams:$archVer" (from T27-MVI/ToDo/app/build.gradle)

This pulls in the needed dependency, using the same version as we are using for the other aspects of the Architecture Components that we are using right now. Next, replace the states field in RosterViewModel, having it be a simple LiveData object instead of a MutableLiveData object: private LiveData<ViewState> states; (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

That, in turn, will require you to get rid of these lines from the RosterViewModel constructor, as they will no longer be relevant (or even compile): ViewState initial=ViewState.builder().items(ToDoRepository.get().all()).build(); states.postValue(initial);

Then, add a new lastState field to RosterViewModel: private ViewState lastState=ViewState.empty().build(); (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

As the name suggests, this is the last ViewState that we published. We initialize it to an empty starting point. Next, add a ReplaySubject field, named stateSubject: private final ReplaySubject<ViewState> stateSubject=ReplaySubject.createWithSize(1);

(from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

A ReplaySubject is like a PublishSubject, except that it caches objects that flow through it. In our case, we ask it to cache the latest one, via the createWithSize(1) factory method.

315


COMPLETING THE MVI FLOW Finally, in the RosterViewModel constructor, after creating the Controller, add this block of code: controller.resultStream() .subscribe(result -> { lastState=foldResultIntoState(lastState, result); stateSubject.onNext(lastState); }, stateSubject::onError); states=LiveDataReactiveStreams .fromPublisher(stateSubject.toFlowable(BackpressureStrategy.LATEST)); (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

This is a bit complicated, so letâ&#x20AC;&#x2122;s take it one piece at a timeâ&#x20AC;Ś We are initializing the LiveData field (states), using a LiveDataReactiveStreams class. This class comes from the dependency that we just added, and it offers a fromPublisher() method that offers the bridge between RxJava and LiveData. Specifically, it creates a LiveData object that knows how to subscribe to an RxJava type and pass any received objects along to its own subscribers. What we pass into fromPublisher() is anything implementing the Publisher interface. Publisher is not from RxJava itself, but instead is part of the Reactive Streams initiative. Reactive Streams publishes its own library of types that specifies an API for this sort of reactive processing. RxJava offers some classes that implement things like Publisher. So, we need to build an RxJava chain, starting from our Result stream from the Controller (controller.resultStream()) that winds up in some type that implements Publisher. And, along the way, we need to map from the Result that we get to the ViewState that we want to publish. Plus, we need to deal with some limitations with LiveData and LiveDataReactiveStreams, where we need to have the stream itself cache the most recently seen ViewState. The map() call handles the Result-to-ViewState part. We use our foldResultIntoState() method to blend our last ViewState with the new Result and get a new ViewState as an outcome. The way we implement the cache is to forward the new ViewState objects along to the ReplaySubject, which is set up to cache the last ViewState that it emits and deliver that ViewState to anything that subscribes to it later.

316


COMPLETING THE MVI FLOW Then, we convert our stateSubject to something that implements Publisher via toFlowable(). Flowable is an RxJava type that resembles Observable but implements the Reactive Streams’ Publisher API. One difference between these APIs is how they handle “backpressure”, roughly defined as “what happens when the pipeline gets clogged?”. In this case, there should be no risk of that happening. But, toFlowable() requires that you supply a BackpressureStrategy to describe what should happen if a clog were to occur. A BackpressureStrategy simply indicates what to do in these cases; BackpressureStrategy.LATEST says “keep the latest items in the pipeline and get rid of older ones if there is a backlog”. So, the net effect of this block of code is to set up our states field with a LiveData that takes an incoming Result from the Controller and publishes a replacement ViewState that reflects the changes dictated by that Result.

Step #5: Handling Modify and Delete Results To handle cases where the user edits or deletes an item, we need to handle Result.Modified results and update the ViewState to match. To that end, add these four methods to ViewState: ViewState modify(ToDoModel model) { List<ToDoModel> models=new new ArrayList<>(items()); ToDoModel original=find(models, model.id()); if (original!=null null) { int index=models.indexOf(original); models.set(index, model); } sort(models); return toBuilder() .items(Collections.unmodifiableList(models)) .current(model) .build(); } ViewState delete(ToDoModel model) { List<ToDoModel> models=new new ArrayList<>(items()); ToDoModel original=find(models, model.id()); if (original==null null) { throw new IllegalArgumentException("Cannot find model to delete: "+model.toString()); } else { models.remove(original); }

317


COMPLETING THE MVI FLOW sort(models); return toBuilder() .items(Collections.unmodifiableList(models)) .current(null null) .build(); }

(from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

private ToDoModel find(List<ToDoModel> models, String id) { int position=findPosition(models, id); return position>=0 ? models.get(position) : null null; } private int findPosition(List<ToDoModel> models, String id) { for (int i=0;i<models.size();i++) { ToDoModel candidate=models.get(i); if (id.equals(candidate.id())) { return i; } } return -1; } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

One issue with the immutable-model approach is that the model that we are being passed to modify is not a model that is already in our ViewState â&#x20AC;&#x201D; it is some new ToDoModel that has changed fields. The ID of the model will be the same, but the actual object instance is not. We need to find the model in our ViewState that has the same ID and take steps to replace it (or remove it, in the case of a deletion). The find() and findPosition() utility methods simply perform an ID-based lookup of the model. modify() and delete() then use that to update a copy of the list, re-sort the list, and build a new ViewState based on that revised list. modify() also sets the current item to be the just-added one, for use by DisplayFragment, while delete() sets the current item to be null, since the previously-current item was just deleted. Next, update foldResultIntoState() in RosterViewModel to handle the Result.Modified and Result.Deleted scenarios:

318


COMPLETING THE MVI FLOW private ViewState foldResultIntoState(@NonNull ViewState state, @NonNull Result result) throws Exception { if (result instanceof Result.Added) { return state.add(((Result.Added)result).model()); } else if (result instanceof Result.Modified) { return state.modify(((Result.Modified)result).model()); } else if (result instanceof Result.Deleted) { return state.delete(((Result.Deleted)result).model()); } else { throw new IllegalStateException("Unexpected result type: "+result.toString()); } }

At this point, our three actions trigger work in our Controller, which emits associated results, which in turn causes our RosterViewModel to emit an updated ViewState, and that triggers a change in our UI.

Step #6: Handling the Initial Load We have two actions that we have ignored: Load and Show. Now, we can start wiring those actions in place, starting with the Load action. We need to emit our Load action when it is time to load our data. Eventually, this will load data out of our database; right now, this will just set up our initial ViewState. A likely time to emit this action is when our RosterViewModel is created, as that instance will be used throughout the lifetime of our activity and fragments, even across configuration changes. So, add this line to the end of the RosterViewModel constructor: process(Action.load()); (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

At this point, the constructor should look like: public RosterViewModel(@NonNull Application application) { super super(application); Controller controller=new new Controller(); controller.resultStream()

319


COMPLETING THE MVI FLOW .subscribe(result -> { lastState=foldResultIntoState(lastState, result); stateSubject.onNext(lastState); }, stateSubject::onError); states=LiveDataReactiveStreams .fromPublisher(stateSubject.toFlowable(BackpressureStrategy.LATEST)); controller.subscribeToActions(actionSubject); process(Action.load()); } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

Next, we need to arrange for our Controller to do the work for loading the data. Eventually, this will require background threads and database I/O, but for now, we can keep it simple. Add this load() method to Controller to create and emit the corresponding Result object: private void load() { resultSubject.onNext(Result.loaded(toDoRepo.all())); } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/Controller.java)

Then, modify processImpl() on Controller to add an else if clause for an Action.Load event: private void processImpl(Action action) { if (action instanceof Action.Add) { add(((Action.Add)action).model()); } else if (action instanceof Action.Edit) { modify(((Action.Edit)action).model()); } else if (action instanceof Action.Delete) { delete(((Action.Delete)action).model()); } else if (action instanceof Action.Load) { load(); } }

Finally, modify foldResultIntoState() on RosterViewModel to handle the Result.Loaded result:

320


COMPLETING THE MVI FLOW private ViewState foldResultIntoState(@NonNull ViewState state, @NonNull Result result) throws Exception { if (result instanceof Result.Added) { return state.add(((Result.Added)result).model()); } else if (result instanceof Result.Modified) { return state.modify(((Result.Modified)result).model()); } else if (result instanceof Result.Deleted) { return state.delete(((Result.Deleted)result).model()); } else if (result instanceof Result.Loaded) { List<ToDoModel> models=((Result.Loaded)result).models(); return ViewState.builder() .items(models) .current(models.size()==0 ? null : models.get(0)) .build(); } else { throw new IllegalStateException("Unexpected result type: "+result.toString()); } }

Here, we get the list of model objects that we “loaded” and use that to set up a new ViewState. In particular, we set the current item to be the first one from the list, if we have any models in the list — otherwise, we set the current model to be null.

Step #7: Showing Items Finally, we can handle Action.Show and Result.Showed. We need to emit the “show” action when the user taps on an item in the list. Right now, we have a showModel() method in RosterListAdapter that tells the MainActivity to switch our fragments around. Let’s reorganize that code a bit and emit the Action.Show event as well. In RosterListFragment, add this method: void showModel(ToDoModel model) { ((RosterListFragment.Contract)getActivity()).showModel(model); viewModel.process(Action.show(model)); } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

321


COMPLETING THE MVI FLOW This moves the call to our MainActivity here, plus adds a process() call to emit our Action.Show action. Then, change showModel() in RosterListAdapter to be: void showModel(ToDoModel model) { host.showModel(model); } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

The adapter tells the fragment to show the model; the fragment tells the activity and the controller to show the model. Our Controller does not have to do much work here, just forwarding along the corresponding Result.Showed event. So, add this show() method to Controller: private void show(ToDoModel current) { resultSubject.onNext(Result.showed(current)); } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/Controller.java)

Then, modify processImpl() to have another else if to route Action.Show events to that show() method: private void processImpl(Action action) { if (action instanceof Action.Add) { add(((Action.Add)action).model()); } else if (action instanceof Action.Edit) { modify(((Action.Edit)action).model()); } else if (action instanceof Action.Delete) { delete(((Action.Delete)action).model()); } else if (action instanceof Action.Load) { load(); } else if (action instanceof Action.Show) { show(((Action.Show)action).current()); } } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/Controller.java)

322


COMPLETING THE MVI FLOW To process the result, we need to emit a new ViewState with all of its current data, but with a different current item. To do this, add a show() method to ViewState: ViewState show(ToDoModel current) { return toBuilder() .current(current) .build(); } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

This just converts the current instance to a builder, replaces its current item with the new value, and builds a replacement ViewState. Then, in RosterViewModel, modify foldResultIntoState() to have a new else if to handle the Result.Showed event, using our new show() method on ViewState: private ViewState foldResultIntoState(@NonNull ViewState state, @NonNull Result result) throws Exception { if (result instanceof Result.Added) { return state.add(((Result.Added)result).model()); } else if (result instanceof Result.Modified) { return state.modify(((Result.Modified)result).model()); } else if (result instanceof Result.Deleted) { return state.delete(((Result.Deleted)result).model()); } else if (result instanceof Result.Loaded) { List<ToDoModel> models=((Result.Loaded)result).models(); return ViewState.builder() .items(models) .current(models.size()==0 ? null : models.get(0)) .build(); } else if (result instanceof Result.Showed) { return state.show(((Result.Showed)result).current()); } else { throw new IllegalStateException("Unexpected result type: "+result.toString()); } }

(from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

So, when the user clicks on the item in the list, we arrange to show that fragment, plus we pass the model through the MVI flow to update our ViewState to reflect that change.

323


COMPLETING THE MVI FLOW

Step #8: Wrapping Up the Rendering We still have a few lingering ToDoRepository references beyond those in Controller that we need to clean up. That is because we are only consuming the ViewState stream in RosterListFragment, not in the other fragments. Add this render() method to DisplayFragment, reminiscent of the render() method we have in RosterListFragment: private void render(ViewState state) { if (state!=null null) { ToDoModel model=state.current(); if (model!=null null) { binding.setModel(model); binding.setCreatedOn(DateUtils.getRelativeDateTimeString(getActivity(), model.createdOn().getTimeInMillis(), DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0)); } } } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/DisplayFragment.java)

If we have a ViewState, we get the current() model from it and use that for the binding (assuming it is not null). Then, replace the existing onViewCreated() method in DisplayFragment with this implementation: @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super super.onViewCreated(view, savedInstanceState); viewModel.stateStream().observe(this this, this this::render); } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/DisplayFragment.java)

Rather than directly referencing ToDoRepository and binding in onViewCreated(), we moved the binding logic to render(). All we need to do in onViewCreated() is start observing our ViewState stream, routing those events to our render() method.

324


COMPLETING THE MVI FLOW Note that getModelId() on DisplayFragment winds up no longer being used at the moment. There are scenarios where we will need that fragment argument, but we will address those in a later tutorial. Next, add this render() method to EditFragment: private void render(ViewState state) { if (state!=null null) { if (getModelId()==null null) { if (deleteMenu!=null null) { deleteMenu.setVisible(false false); } } else { ToDoModel model=state.current(); binding.setModel(model); } } } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

This is a bit different than what we had with DisplayFragment: • We are not showing the created-on timestamp and so do not have to format that value and put it on the binding • We have to deal with add and delete being separate scenarios, and we tell those apart by whether or not we were passed an ID of a model via the newInstance() factory method (and made available via getModelId()) • We want to toggle the visibility of the “delete” action bar item based on whether this is an add or edit operation, though we can only do that if the menu is ready at the time render() is called This code will not compile, as we do not have a deleteItem to work with. So, add that field to EditFragment: private MenuItem deleteMenu; (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

And modify onCreateOptionsMenu() to initialize it: @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {

325


COMPLETING THE MVI FLOW inflater.inflate(R.menu.actions_edit, menu); deleteMenu=menu.findItem(R.id.delete); deleteMenu.setVisible(getModelId()!=null null); super super.onCreateOptionsMenu(menu, inflater); } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

Then, replace the onViewCreated() method on EditFragment with the same implementation that we used in DisplayFragment: @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super super.onViewCreated(view, savedInstanceState); viewModel.stateStream().observe(this this, this this::render); } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/EditFragment.java)

Also, in RosterListFragment, add an else clause to the if in render(): public void render(ViewState state) { adapter.setState(state); if (rv.getAdapter().getItemCount()>0) { empty.setVisibility(View.GONE); } else { empty.setVisibility(View.VISIBLE); } } (from T27-MVI/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

This will handle the case where we had items previously, but deleted the last one, and so now we need to flip the visibility of the empty View back to VISIBLE.

Step #9: Trying It Out At this point â&#x20AC;&#x201D; many tutorials into the architecture revamp â&#x20AC;&#x201D; your app should now work again, just as it did before. All of the existing behavior, such as adding, editing, and removing items, should work without issue.

326


COMPLETING THE MVI FLOW

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • • • • • • • • •

app/src/main/java/com/commonsware/todo/Controller.java app/src/main/java/com/commonsware/todo/ViewState.java app/src/main/java/com/commonsware/todo/ToDoModel.java app/src/main/java/com/commonsware/todo/RosterViewModel.java app/build.gradle app/src/main/java/com/commonsware/todo/RosterListFragment.java app/src/main/java/com/commonsware/todo/RosterListAdapter.java app/src/main/java/com/commonsware/todo/DisplayFragment.java app/src/main/java/com/commonsware/todo/EditFragment.java

327


Testing the MVI Flow

You should be able to run the RepoTests and confirm that they still work, as we have not made material changes to the repository… yet. We will be revising it a fair bit coming up in a future tutorial, as we start to save our items in a database. However, we should start testing some of our other functionality. In this tutorial, we will write a ControllerTest test class and method for testing our Controller, ensuring that it can receive actions and publish results. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Step #1: Adding a ControllerTest Class We need to add a new Java class, which you have done many times already. However, this time, we need to add it to the androidTest source set, not to the main source set. Right-click over the com.commonsware.todo package in the java/ directory in the androidTest source set, where you have RepoTests. Choose “New” > “Java Class” from the context menu. For the name, fill in ControllerTest. Click OK to create the class, giving you: package com.commonsware.todo; public class ControllerTest { }

329


TESTING THE MVI FLOW Then, add the @RunWith(AndroidJUnit4.class) that we have on RepoTests to ControllerTest, giving you: package com.commonsware.todo; @RunWith(AndroidJUnit4.class) public class ControllerTest { }

Next, add a stub @Test method, named controller(), giving you: package com.commonsware.todo; @RunWith(AndroidJUnit4.class) public class ControllerTest { @Test public void controller() { } }

If you run ControllerTest via the double-play icon in the gutter next on the public class ControllerTest line, it should run successfully… albeit while not testing anything.

Step #2: Setting Up a Controller Next, add these lines to your new controller() method: final Controller controller=new new Controller(); final PublishSubject<Action> actionSubject=PublishSubject.create(); final LinkedBlockingQueue<Result> receivedResults=new new LinkedBlockingQueue<>(); controller.subscribeToActions(actionSubject); controller.resultStream().subscribe(receivedResults::offer);

(from T28-MVITest/ToDo/app/src/androidTest/java/com/commonsware/todo/ControllerTest.java)

Here, we: • Set up a Controller instance to use for our testing • Set up a PublishSubject to serve as our source of actions, just like the one a RosterViewModel might have • Set up a LinkedBlockingQueue to be the “sink” of results that we receive from the Controller 330


TESTING THE MVI FLOW • Have the Controller subscribe to our stream of actions • Subscribe to the stream of results from the Controller, pushing each into the LinkedBlockingQueue, by calling its offer() method (and using Java 8 method references to be able to use that as an RxJava subscriber) Now, if we call onNext() on the actionSubject to supply it with an action, we can see if our Result shows up in our LinkedBlockingQueue.

Step #3: Testing the Initial Load Next, let’s confirm that we can process a Loaded action. Add these lines to controller(), after the lines from the preceding step: actionSubject.onNext(Action.load()); Result.Loaded resultLoaded= (Result.Loaded)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(0, resultLoaded.models().size()); (from T28-MVITest/ToDo/app/src/androidTest/java/com/commonsware/todo/ControllerTest.java)

This code: • Emits a Loaded action onto our stream • Waits for up to a second for a Result to arrive from its stream • Confirms (via the cast) that it is a Result.Loaded object and that we have no models (because we are starting with an empty repository) You will need to add throws InterruptedException to the controller() method declaration. This would be thrown if the Controller fails to deliver a Result within one second. At this point, your controller() method should resemble: @Test public void controller() throws InterruptedException { final Controller controller=new new Controller(); final PublishSubject<Action> actionSubject=PublishSubject.create(); final LinkedBlockingQueue<Result> receivedResults=new new LinkedBlockingQueue<>(); controller.subscribeToActions(actionSubject); controller.resultStream().subscribe(receivedResults::offer);

331


TESTING THE MVI FLOW actionSubject.onNext(Action.load()); Result.Loaded resultLoaded= (Result.Loaded)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(0, resultLoaded.models().size()); }

And, if you run the ControllerTest, your test should still succeed.

Step #4: Testing Adds Next, letâ&#x20AC;&#x2122;s add a couple of items and confirm that our results reflect the added items. Add these lines to controller(), after the lines that are already there: final ToDoModel fooModel=ToDoModel.creator().description("foo").notes("hello, world!").build(); actionSubject.onNext(Action.add(fooModel)); Result.Added resultAdded= (Result.Added)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(fooModel, resultAdded.model()); final ToDoModel barModel=ToDoModel.creator().description("bar").build(); actionSubject.onNext(Action.add(barModel)); resultAdded= (Result.Added)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(barModel, resultAdded.model()); final ToDoModel gooModel=ToDoModel.creator() .description("goo") .isCompleted(true true) .build(); actionSubject.onNext(Action.add(gooModel)); resultAdded= (Result.Added)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(gooModel, resultAdded.model());

(from T28-MVITest/ToDo/app/src/androidTest/java/com/commonsware/todo/ControllerTest.java)

Here, we create three models (fooModel, barModel, and gooModel). We use Action.add() to add each of them to our repository. And we confirm that we get an equivalent model back in the result. Once again, if you run ControllerTest, it should succeed.

332


TESTING THE MVI FLOW

Step #5: Testing Modifications Next, letâ&#x20AC;&#x2122;s confirm that we can modify these models. Add these lines to controller(), after the ones that are already there: final ToDoModel mutatedFoo=fooModel.toBuilder().isCompleted(true true).build(); actionSubject.onNext(Action.edit(mutatedFoo)); Result.Modified resultModified= (Result.Modified)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(mutatedFoo, resultModified.model()); final ToDoModel mutatedBar=barModel.toBuilder().description("bar!").notes("hi!").build(); actionSubject.onNext(Action.edit(mutatedBar)); resultModified= (Result.Modified)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(mutatedBar, resultModified.model()); final ToDoModel mutatedGoo=gooModel.toBuilder() .description("goo!") .isCompleted(false false) .build(); actionSubject.onNext(Action.edit(mutatedGoo)); resultModified= (Result.Modified)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(mutatedGoo, resultModified.model());

(from T28-MVITest/ToDo/app/src/androidTest/java/com/commonsware/todo/ControllerTest.java)

We modify each of the three models, by converting each back to a Builder, making changes, and building the resulting revised model. We use Action.edit() to update the repository with the revised models, and we confirm that the Result that we get back has an equivalent modified model. As before, if you run ControllerTest, it should succeed.

Step #6: Testing Deletions This leaves one more operation to test: deletion. Add these lines to controller(), after the ones that you have already added: actionSubject.onNext(Action.delete(barModel)); Result.Deleted resultDeleted= (Result.Deleted)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(barModel, resultDeleted.model());

333


TESTING THE MVI FLOW actionSubject.onNext(Action.delete(fooModel)); resultDeleted= (Result.Deleted)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(fooModel, resultDeleted.model()); actionSubject.onNext(Action.delete(gooModel)); resultDeleted= (Result.Deleted)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(gooModel, resultDeleted.model()); (from T28-MVITest/ToDo/app/src/androidTest/java/com/commonsware/todo/ControllerTest.java)

We delete our three models, confirming that the Result objects show what we asked to be deleted. At this point, your entire controller() method should look like: package com.commonsware.todo; import import import import import import import

android.support.test.runner.AndroidJUnit4 android.support.test.runner.AndroidJUnit4; org.junit.Test org.junit.Test; org.junit.runner.RunWith org.junit.runner.RunWith; java.util.concurrent.LinkedBlockingQueue java.util.concurrent.LinkedBlockingQueue; java.util.concurrent.TimeUnit java.util.concurrent.TimeUnit; io.reactivex.subjects.PublishSubject io.reactivex.subjects.PublishSubject; static org.junit.Assert.assertEquals;

@RunWith(AndroidJUnit4.class) public class ControllerTest { @Test public void controller() throws InterruptedException { final Controller controller=new new Controller(); final PublishSubject<Action> actionSubject=PublishSubject.create(); final LinkedBlockingQueue<Result> receivedResults=new new LinkedBlockingQueue<>(); controller.subscribeToActions(actionSubject); controller.resultStream().subscribe(receivedResults::offer); actionSubject.onNext(Action.load()); Result.Loaded resultLoaded= (Result.Loaded)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(0, resultLoaded.models().size()); final ToDoModel fooModel=ToDoModel.creator().description("foo").notes("hello, world!").build(); actionSubject.onNext(Action.add(fooModel)); Result.Added resultAdded= (Result.Added)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(fooModel, resultAdded.model());

334


TESTING THE MVI FLOW final ToDoModel barModel=ToDoModel.creator().description("bar").build(); actionSubject.onNext(Action.add(barModel)); resultAdded= (Result.Added)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(barModel, resultAdded.model()); final ToDoModel gooModel=ToDoModel.creator() .description("goo") .isCompleted(true true) .build(); actionSubject.onNext(Action.add(gooModel)); resultAdded= (Result.Added)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(gooModel, resultAdded.model()); final ToDoModel mutatedFoo=fooModel.toBuilder().isCompleted(true true).build(); actionSubject.onNext(Action.edit(mutatedFoo)); Result.Modified resultModified= (Result.Modified)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(mutatedFoo, resultModified.model()); final ToDoModel mutatedBar=barModel.toBuilder().description("bar!").notes("hi!").build(); actionSubject.onNext(Action.edit(mutatedBar)); resultModified= (Result.Modified)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(mutatedBar, resultModified.model()); final ToDoModel mutatedGoo=gooModel.toBuilder() .description("goo!") .isCompleted(false false) .build(); actionSubject.onNext(Action.edit(mutatedGoo)); resultModified= (Result.Modified)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(mutatedGoo, resultModified.model()); actionSubject.onNext(Action.delete(barModel)); Result.Deleted resultDeleted= (Result.Deleted)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(barModel, resultDeleted.model()); actionSubject.onNext(Action.delete(fooModel)); resultDeleted= (Result.Deleted)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(fooModel, resultDeleted.model()); actionSubject.onNext(Action.delete(gooModel)); resultDeleted= (Result.Deleted)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(gooModel, resultDeleted.model()); } }

(from T28-MVITest/ToDo/app/src/androidTest/java/com/commonsware/todo/ControllerTest.java)

335


TESTING THE MVI FLOW And, if you run ControllerTest, it should work.

What We Changed The bookâ&#x20AC;&#x2122;s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the one file that we changed: app/src/ androidTest/java/com/commonsware/todo/ControllerTest.java

336


Getting a Room

So far, we have been content to have our to-do items vanish when we re-run our app. This was simple and easy to write. However, it is not realistic. Users will expect their to-do items to remain until deleted. To do that, we need our items need to survive process termination, and that requires that we save those items somewhere, such as on disk. In this tutorial, we will start in on that work, setting up database support using Room, a Google-supplied framework that layers atop Android’s native SQLite support. SQLite is a relational database. Through Room, we will create a database containing a table for our to-do items. In truth, this app is trivial enough that you could use something simpler for storage, such as storing the items in a JSON file. The bigger the app, the more likely it is that SQLite and Room will be better options for you. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Read Me! Google has official documentation for Room, though it seems to have lost its former documentation for SQLite itself. You can read more about SQLite in The Busy Coder’s Guide to Android Development, specifically in: • a chapter on SQLite basics 337


GETTING A ROOM • a chapter on SQLCipher for Android, an encrypted version of SQLite • a chapter on advanced database techniques You can read more about Room in several chapters of Android’s Architecture Components, including: • • • •

the basics of setting up Room details on how to set up a data-access object, or DAO setting up relations between tables using Room with RxJava

Step #1: Requesting More Dependencies Room has its own set of dependencies that we need to add to the dependencies closure in app/build.gradle. Room has its own series of versions, independent of anything else that we have used. So, let’s define another version constant: def roomVer="1.1.0" (from T29-Room/ToDo/app/build.gradle)

Then, add three new dependencies that reference that version constant: implementation "android.arch.persistence.room:runtime:$roomVer" implementation "android.arch.persistence.room:rxjava2:$roomVer" annotationProcessor "android.arch.persistence.room:compiler:$roomVer" (from T29-Room/ToDo/app/build.gradle)

Room is based heavily on the use of Java annotations, and the compiler artifact will handle those annotations for us at compile time. The runtime dependency is for core Room functionality, while the rxjava2 dependency makes it easier for use to blend Room with RxJava. After adding these lines, go ahead and allow Android Studio to sync the project with the Gradle build files.

338


GETTING A ROOM

Step #2: Defining an Entity In Room, an entity is a Java class that is our in-memory representation of a SQLite table. Instances of the entity class represent rows in that table. So, we need an entity to create a SQLite table for our to-do items. Which means… we need another Java class! Once again, right-click over the com.commonsware.todo package in the java/ directory and choose “New” > “Java Class” from the context menu. For the name, fill in ToDoEntity. Click OK to create the class, giving you: package com.commonsware.todo; public class ToDoEntity { }

Then, replace that stub implementation with this: package com.commonsware.todo; import import import import import

android.arch.persistence.room.Entity android.arch.persistence.room.Entity; android.arch.persistence.room.Index android.arch.persistence.room.Index; android.arch.persistence.room.PrimaryKey android.arch.persistence.room.PrimaryKey; android.support.annotation.NonNull android.support.annotation.NonNull; java.util.Calendar java.util.Calendar;

@Entity(tableName="todos", indices=@Index(value="id")) public class ToDoEntity { @PrimaryKey @NonNull final String id; @NonNull final String description; final String notes; final boolean isCompleted; @NonNull final Calendar createdOn; ToDoEntity(@NonNull String id, @NonNull String description, boolean isCompleted, String notes, @NonNull Calendar createdOn) { this this.id=id; this this.description=description; this this.isCompleted=isCompleted; this this.notes=notes; this this.createdOn=createdOn; } }

339


GETTING A ROOM This class has the same fields as ToDoModel. You might wonder why we didn’t just use ToDoModel. Partly, that is for realism: while in this case models and entities have a 1:1 correspondence, that is not always the case. Partly, this choice was made because Room does not work with AutoValue, and so we cannot make our entity instances truly immutable… though we come close. What makes this class an entity is the @Entity annotation at the top. There, we can provide metadata about the table that we want to have created. Here, we specify two things: 1. We want the underlying table name to be todoes, as opposed to the default, which is the same as the class name (ToDoEntity) 2. We want to create an index on our id column, since that is our primary key, and so we may be referencing that column a lot Room knows that id is our primary key because we have the @PrimaryKey annotation on its field. Room wants us to declare some primary key, typically via that @PrimaryKey annotation. We use @NonNull annotations to tell Room to make those columns be NOT NULL, preventing null values from being stored in them. Note that our isCompleted column could have @NonNull on it, but that is not necessary, as a boolean can never be null in the first place. Beyond that, we have a constructor, one that accepts all of the values for our fields, with parameter names matching the field names. Room demands that we either have such a constructor or that it can write to the fields themselves.

Step #3: Crafting a DAO The @Entity class says “this is what my table should look like”. A @Dao class says “this is how I want to read and write from that table”. With Room, we define an interface or abstract class to describe the API that we want to have for working with the database. Room then code-generates an implementation for us, dealing with all of the SQLite code for getting our entities to and from our table. Inside the ToDoEntity class, add this nested interface: @Dao public interface Store { @Query("SELECT * FROM todos ORDER BY description ASC")

340


GETTING A ROOM List<ToDoEntity> all(); @Insert void insert(ToDoEntity... entities); @Update void update(ToDoEntity... entities); @Delete void delete(ToDoEntity... entities); } (from T29-Room/ToDo/app/src/main/java/com/commonsware/todo/ToDoEntity.java)

The @Dao annotation tells Room that this class serves as a DAO and defines an API that we want to use. On it, we have four methods. Each has an annotation indicating what is the database operation that this method should apply: • • • •

@Insert for inserts @Update for updates @Delete for deletions @Query for anything, but

mostly used for data retrieval

Usually, we do not need any annotation properties on @Insert, @Update, or @Delete methods. The type of the method parameter will be based on the entity whose table it is that we wish to insert into, update, or delete from. That parameter type can be a single entity, a List of entities, or a varargs of entities. In this case, the Store uses varargs, so we can pass 1+ entities as parameters, and they will all be inserted, updated, or deleted in unison. The @Query annotation takes a string that spells out the SQL statement to be executed. Here, we just ask for everything from our todos table, ordered by the description column in ascending order. Room supports more sophisticated @Query methods, such as ones that take parameters that can get spliced into the SELECT statement, but this is all that we need for now. Note that all of these methods are synchronous. We could have the all() method be asynchronous, by having it return an RxJava Observable or something wrapped around the results. In that case, the actual query work would not be done until we subscribed the Observable, and we could arrange for it to do its work on a background thread. In our case, the responsibility for background threads lies more in the Controller and Repository, and so we do not need to bother with this here.

341


GETTING A ROOM The entire ToDoEntity class should now look like: package com.commonsware.todo; import import import import import import import import import import import

android.arch.persistence.room.Dao android.arch.persistence.room.Dao; android.arch.persistence.room.Delete android.arch.persistence.room.Delete; android.arch.persistence.room.Entity android.arch.persistence.room.Entity; android.arch.persistence.room.Index android.arch.persistence.room.Index; android.arch.persistence.room.Insert android.arch.persistence.room.Insert; android.arch.persistence.room.PrimaryKey android.arch.persistence.room.PrimaryKey; android.arch.persistence.room.Query android.arch.persistence.room.Query; android.arch.persistence.room.Update android.arch.persistence.room.Update; android.support.annotation.NonNull android.support.annotation.NonNull; java.util.Calendar java.util.Calendar; java.util.List java.util.List;

@Entity(tableName="todos", indices=@Index(value="id")) public class ToDoEntity { @PrimaryKey @NonNull final String id; @NonNull final String description; final String notes; final boolean isCompleted; @NonNull final Calendar createdOn; ToDoEntity(@NonNull String id, @NonNull String description, boolean isCompleted, String notes, @NonNull Calendar createdOn) { this this.id=id; this this.description=description; this this.isCompleted=isCompleted; this this.notes=notes; this this.createdOn=createdOn; } @Dao public interface Store { @Query("SELECT * FROM todos ORDER BY description ASC") List<ToDoEntity> all(); @Insert void insert(ToDoEntity... entities); @Update void update(ToDoEntity... entities); @Delete void delete(ToDoEntity... entities); } }

(from T29-Room/ToDo/app/src/main/java/com/commonsware/todo/ToDoEntity.java)

342


GETTING A ROOM

Step #4: Adding a Database (And Some Type Converters) The third major piece of any Room usage is a @Database. Here, we not only need to add the annotation to a Java class, but we need to have that class inherit from Room’s own RoomDatabase base class. Which means… we need another Java class! Again! Right-click over the com.commonsware.todo package in the java/ directory and choose “New” > “Java Class” from the context menu. For the name, fill in ToDoDatabase. Set the superclass to be android.arch.persistence.room.RoomDatabase – if you start typing in RoomDatabase, you should get an auto-completion option for this. Mark the class as abstract. Then, click OK to create the class, giving you: package com.commonsware.todo; import android.arch.persistence.room.RoomDatabase android.arch.persistence.room.RoomDatabase; public abstract class ToDoDatabase extends RoomDatabase { }

Then, replace that implementation with: package com.commonsware.todo; import import import import

android.arch.persistence.room.Database android.arch.persistence.room.Database; android.arch.persistence.room.Room android.arch.persistence.room.Room; android.arch.persistence.room.RoomDatabase android.arch.persistence.room.RoomDatabase; android.content.Context android.content.Context;

@Database(entities={ToDoEntity.class}, version=1) public abstract class ToDoDatabase extends RoomDatabase { public abstract ToDoEntity.Store todoStore(); private static final String DB_NAME="stuff.db"; private static volatile ToDoDatabase INSTANCE=null null; synchronized static ToDoDatabase get(Context ctxt) { if (INSTANCE==null null) { INSTANCE=create(ctxt); }

343


GETTING A ROOM return INSTANCE; } private static ToDoDatabase create(Context ctxt) { RoomDatabase.Builder<ToDoDatabase> b= Room.databaseBuilder(ctxt.getApplicationContext(), ToDoDatabase.class, DB_NAME); return b.build(); } }

The @Database annotation is where we provide metadata about the database that we want Room to manage for us. Specifically: • We tell it which classes have @Entity annotations and should have their tables in this database • What is the version code of this database schema — usually, we start at 1, and we increment from there, any time that we add tables, columns, indices, and so on The todoStore() method returns an instance of our @Dao-annotated interface. This, coupled with the @Database annotation, tells Room’s annotation processor to codegenerate an implementation of ToDoDatabase that has an implementation of todoStore() that returns a code-generated implementation of ToDoEntity.Store. Frequently, a RoomDatabase is used as a singleton. So, we have a static field named INSTANCE to hold that singleton, which we lazy-create the first time something calls the get() method. get() is synchronized, so only one thread at a time can execute get(), to help prevent us from accidentally creating two ToDoDatabase instances. get(),

in turn, delegates the actual work of creating the database to create(). To create the ToDoDatabase instance, we use Room.databaseBuilder(), passing it three values: • a Context to use — and since this is a singleton, we need to use the Application to avoid any memory leaks • the class representing the RoomDatabase to create • a String with the filename to use for the database The resulting RoomDatabase.Builder could be further configured, but we do not need that here, so we just have it build() the database and return it.

344


GETTING A ROOM If you try building the project — for example, Build > “Make module ‘app’” from the Android Studio main menu — you will get a bunch of confusing error messages. Many of those are code-generated things complaining that other code-generated things do not exist. The root error is: Cannot figure out how to save this field into database. You can consider adding a type converter for it.

Double-clicking on that error in the Messages view leads you to the declaration of the createdOn field in ToDoEntity. The problems is that Room does not know what to do with a Calendar object. SQLite does not have a native date/time column type, and Room cannot convert arbitrary objects into arbitrary SQLite column types. Instead, Room’s annotation processor detects the issue and fails the build. To fix this, we need to teach Room how to convert Calendar objects to and from some standard SQLite column type. And for that… we could really use another Java class. Fortunately, you can never have too many Java classes! Right-click over the com.commonsware.todo package in the java/ directory and choose “New” > “Java Class” from the context menu. For the name, fill in TypeTransmogrifier. Click OK to create the class, giving you: package com.commonsware.todo; public class TypeTransmogrifier { }

A transmogrifier is a ~30-year-old piece of advanced technology that can convert one thing into another. Here, we are creating a type transmogrifier: a set of methods that turn one type into another. To that end, replace the stub generated class with this: package com.commonsware.todo; import android.arch.persistence.room.TypeConverter android.arch.persistence.room.TypeConverter; import java.util.Calendar java.util.Calendar; public class TypeTransmogrifier { @TypeConverter

345


GETTING A ROOM public static Long fromCalendar(Calendar date) { if (date==null null) { return null null; } return date.getTimeInMillis(); } @TypeConverter public static Calendar toCalendar(Long millisSinceEpoch) { if (millisSinceEpoch==null null) { return null null; } Calendar result=Calendar.getInstance(); result.setTimeInMillis(millisSinceEpoch); return result; } } (from T29-Room/ToDo/app/src/main/java/com/commonsware/todo/TypeTransmogrifier.java)

The @TypeConverter annotations tell Room that this is a method that can convert one type into another. Here, we convert Calendar objects into Long objects, using the time-since-the-Unix-epoch methods on Calendar. Then, add this annotation to the ToDoDatabase class declaration, under the existing @Database annotation: @TypeConverters({TypeTransmogrifier.class})

This tells Room that for any entities used by this ToDoDatabase, if you need to convert a type, try looking for @TypeConverter methods on TypeTransmogrifier. Now, if you choose Build > “Make module ‘app’” from the Android Studio main menu, the app should build successfully. Of course, we are not using these new classes… but we will do so in the next tutorial.

346


GETTING A ROOM

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • • • •

app/build.gradle app/src/main/java/com/commonsware/todo/ToDoEntity.java app/src/main/java/com/commonsware/todo/ToDoDatabase.java app/src/main/java/com/commonsware/todo/TypeTransmogrifier.java

347


Integrating Room Into the Repository

Having a ToDoDatabase is nice. Having our ToDoRepository use that ToDoDatabase would be even better, as then we would start saving our to-do items to a database, so they would not vanish every time our process is terminated. So, in this tutorial, we will do just that: modify ToDoRepository – and its clients — to work with ToDoDatabase. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Step #1: Getting a Database First, we need to have our ToDoRepository get access to a ToDoDatabase. Add this field to ToDoRepository: private final ToDoDatabase db; (from T30-RoomRepo/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

This gives us a place to hold onto the database. However, Android Studio got angry when you added it, pointing out that we are not initializing this final field. So, add this constructor to ToDoRepository: private ToDoRepository(Context ctxt) { db=ToDoDatabase.get(ctxt); } (from T30-RoomRepo/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

349


INTEGRATING ROOM INTO THE REPOSITORY Now, given a Context, we can get our singleton lazy-initialized instance of ToDoDatabase. However, Android Studio got angry when you added that, because we are trying to use a zero-argument constructor in our INSTANCE field initializer. That constructor no longer exists, as we added a non-zero-argument constructor, eliminating the default zero-argument constructor. Plus, we need a Context. So, change INSTANCE on ToDoRepository to be initialized to null: private static volatile ToDoRepository INSTANCE=null null; (from T30-RoomRepo/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

Then, we need to modify the static get() method on ToDoRepository to initialize the INSTANCE: public synchronized static ToDoRepository get(Context ctxt) { if (INSTANCE==null null) { INSTANCE=new new ToDoRepository(ctxt.getApplicationContext()); } return INSTANCE; } (from T30-RoomRepo/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

Now, we require that get() be passed a Context, so we can turn around and use that to create our ToDoRepository instance. In truth, we could skip the getApplicationContext() call here, as we know that ToDoDatabase has its own getApplicationContext() call, but it is here for safetyâ&#x20AC;&#x2122;s sake. Android Studio is now angry in other places in your code, as references to the old zero-parameter get() method fail. Chief among those is Controller, which tries to initialize its toDoRepo field using that zero-parameter get() method. So, remove the field initializer, leaving you with: private final ToDoRepository toDoRepo; (from T30-RoomRepo/ToDo/app/src/main/java/com/commonsware/todo/Controller.java)

Now Android Studio complains that we are not initializing this final field. So, add a Controller constructor to initialize it, using a passed-in Context: 350


INTEGRATING ROOM INTO THE REPOSITORY public Controller(Context ctxt) { toDoRepo=ToDoRepository.get(ctxt); } (from T30-RoomRepo/ToDo/app/src/main/java/com/commonsware/todo/Controller.java)

Of course, now Android Studio will complain wherever we are using the former zero-parameter Controller constructor, as that’s now gone. The primary use of Controller is inside the RosterViewModel constructor. Fortunately, we have access to a Context there, in the form of the Application parameter. So, modify the RosterViewModel constructor to use that Application when creating the Controller instance: Controller controller=new new Controller(application); (from T30-RoomRepo/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

At this point, the app’s main code is happy. However, our tests have lingering issues. First, change the setUp() method on RepoTests to use ToDoRepository.get() and a Context to get the ToDoRepository instance: @Before public void setUp() { repo=ToDoRepository.get(InstrumentationRegistry.getTargetContext()); repo.add(ToDoModel.creator() .description("Buy a copy of _Exploring Android_") .notes("See https://wares.commonsware.com") .isCompleted(true true) .build()); repo.add(ToDoModel.creator() .description("Complete all of the tutorials") .build()); repo.add(ToDoModel.creator() .description("Write an app for somebody in my community") .notes("Talk to some people at non-profit organizations to see what they need!") .build()); }

InstrumentationRegistry.getTargetContext()

is a wordy way of getting a Context

object for the app being tested (the “target”). Similarly, pass in InstrumentationRegistry.getTargetContext() to the Controller constructor invocation in the controller() method of ControllerTest: 351


INTEGRATING ROOM INTO THE REPOSITORY final Controller controller=new new Controller(InstrumentationRegistry.getTargetContext());

(from T30-RoomRepo/ToDo/app/src/androidTest/java/com/commonsware/todo/ControllerTest.java)

At this point, the project should build and the tests should work… mainly because we are not actually using the database.

Step #2: Fixing the CRUD Now, we need to have the ToDoRepository really use the ToDoDatabase, rather than just hold onto it. However, we have a slight issue. ToDoRepository works with models. ToDoDatabase – via ToDoEntity.Store — works with entities. We are going to need to be able to convert between these two types. To that end, add this factory method to ToDoEntity: public static ToDoEntity fromModel(ToDoModel model) { return new ToDoEntity(model.id(), model.description(), model.isCompleted(), model.notes(), model.createdOn()); } (from T30-RoomRepo/ToDo/app/src/main/java/com/commonsware/todo/ToDoEntity.java)

This creates a ToDoEntity object with the same data as was found in the supplied ToDoModel. Next, add this method to ToDoEntity, which goes in the opposite direction: converting an entity to a model: public ToDoModel toModel() { return ToDoModel.builder() .id(id) .description(description) .isCompleted(isCompleted) .notes(notes) .createdOn(createdOn) .build(); } (from T30-RoomRepo/ToDo/app/src/main/java/com/commonsware/todo/ToDoEntity.java)

Then, replace the all(), add(), replace(), and delete() methods on ToDoRepository with the following: 352


INTEGRATING ROOM INTO THE REPOSITORY public List<ToDoModel> all() { List<ToDoEntity> entities=db.todoStore().all(); ArrayList<ToDoModel> result=new new ArrayList<>(entities.size()); for (ToDoEntity entity : entities) { result.add(entity.toModel()); } return result; } public void add(ToDoModel model) { db.todoStore().insert(ToDoEntity.fromModel(model)); } public void replace(ToDoModel model) { db.todoStore().update(ToDoEntity.fromModel(model)); } public void delete(ToDoModel model) { db.todoStore().delete(ToDoEntity.fromModel(model)); } (from T30-RoomRepo/ToDo/app/src/main/java/com/commonsware/todo/ToDoRepository.java)

Here, in each case, we call through to the ToDoEntity.Store, converting between models and entities as needed. At this point, you can delete the items list, as we no longer need it. You can also delete the now-unused find() method. At this point, if you try out the app, it should mostly work as beforeâ&#x20AC;Ś except that it will remember your to-do items, even if you terminate your process (e.g., swipe the app off of the overview screen). The tests, however, need a bit more work.

Step #3: Fixing the Tests One problem with storing our items in a database is that those items stick around, even after our process terminates. For our production app, this is a feature, not a bug. But, for our tests, this is a problem, as we want to start from a clean slate each time.

353


INTEGRATING ROOM INTO THE REPOSITORY There are a few solutions to this problem. Right now, we will take the simple approach of deleting everything from the database before each test case, plus using a separate application ID for our test app compared to our main app. Let’s work on that latter problem first. In app/build.gradle, add this line to your defaultConfig closure: testApplicationId "com.commonsware.todo.test" (from T30-RoomRepo/ToDo/app/build.gradle)

This says that the application ID for our test app should be com.commonsware.todo.test, compared to the com.commonsware.test that we have for the applicationId property. This will allow our test code to be independent of our main app, including having separate databases. When Android Studio asks, allow it to sync the project with the Gradle build files. Next, add this method to ToDoEntity.Store: @Query("DELETE FROM todos") void deleteAll(); (from T30-RoomRepo/ToDo/app/src/main/java/com/commonsware/todo/ToDoEntity.java)

This adds another DAO method, one that will delete everything from the todos table. @Query annotations can be used for a wide range of SQL statements, including DELETE statements. Our existing delete() method requires us to have the entities to be deleted. While we could use all() to get those – winding up with delete(all()) to delete the table contents — it is faster to just do that in a single SQL statement. Then, on ControllerTest, add this method: @Before public void setUp() { ToDoDatabase db=ToDoDatabase.get(InstrumentationRegistry.getTargetContext()); db.todoStore().deleteAll(); }

(from T30-RoomRepo/ToDo/app/src/androidTest/java/com/commonsware/todo/ControllerTest.java)

As with the @Before-annotated method on RepoTests, this setUp() method will be called before each test method. We just get() our database and call the new deleteAll() method, so we are starting from an empty state. If you run ControllerTest, it should succeed. 354


INTEGRATING ROOM INTO THE REPOSITORY In RepoTests, add a call to deleteAll() at the top of the setUp() method: @Before public void setUp() { ToDoDatabase db=ToDoDatabase.get(InstrumentationRegistry.getTargetContext()); db.todoStore().deleteAll(); repo=ToDoRepository.get(InstrumentationRegistry.getTargetContext()); repo.add(ToDoModel.creator() .description("Buy a copy of _Exploring Android_") .notes("See https://wares.commonsware.com") .isCompleted(true true) .build()); repo.add(ToDoModel.creator() .description("Complete all of the tutorials") .build()); repo.add(ToDoModel.creator() .description("Write an app for somebody in my community") .notes("Talk to some people at non-profit organizations to see what they need!") .build()); }

(from T30-RoomRepo/ToDo/app/src/androidTest/java/com/commonsware/todo/RepoTests.java)

We also need to make some changes to the replace() test case. First, our test description is a bit old. More importantly, since all() on the repository goes to the database, the ToDoModel objects that it returns are brand new. So, the assertSame() test at the end of the method needs to be changed to assertEquals(), which will compare the values inside of the models, rather than confirming that the two references are to the same objects. With that in mind, change replace() to be: @Test public void replace() { ToDoModel original=repo.all().get(1); ToDoModel edited=original.toBuilder() .isCompleted(true true) .description("Currently on Tutorial #30") .build(); repo.replace(edited); assertEquals(3, repo.all().size()); assertEquals(edited, repo.all().get(1)); } (from T30-RoomRepo/ToDo/app/src/androidTest/java/com/commonsware/todo/RepoTests.java)

And, if you run RepoTests, everything should succeed.

355


INTEGRATING ROOM INTO THE REPOSITORY

Step #4: Integrating StrictMode Now that we are doing disk I/O, we ought to confirm that we are avoiding doing that I/O on the main application thread. The main application thread is what drives our UI. Every millisecond that we spend doing unfortunate things on that thread is a millisecond that our UI is frozen. In general, avoid I/O on the main application thread. StrictMode

can yell at us when we accidentally do such I/O on the main application thread. However, StrictMode does not complain about disk I/O by default, so we need to enable that. First, we are going to create a custom subclass of Application. There is a singleton instance of Application (or a subclass) in your process, created when your process starts up. For initialization work that needs to occur for the entire process, Application is a good place for that work to reside. Right-click over the com.commonsware.todo package in the java/ directory and choose “New” > “Java Class” from the context menu. For the name, fill in ToDoApp. Set the superclass to be android.app.Application — if you start typing in Application, you should get an auto-complete option for this class. Then, click OK to create the class, giving you: package com.commonsware.todo; import android.app.Application android.app.Application; public class ToDoApp extends Application { }

Just because we create a subclass of Application does not cause that subclass to be used for this singleton. To accomplish that, we need to make a change to the manifest. So, in app/src/main/AndroidManifest.xml, add the android:name attribute to the <application> element, setting its value to .ToDoApp: <application android:name=".ToDoApp" android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true"

356


INTEGRATING ROOM INTO THE REPOSITORY android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> > <activity android:name=".MainActivity"> > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".AboutActivity"></activity> ></activity> </application> (from T30-RoomRepo/ToDo/app/src/main/AndroidManifest.xml)

Now, when Android wants to set up the Application singleton, it will use our ToDoApp subclass rather than the default. Then, change ToDoApp to be the following: package com.commonsware.todo; import import import import

android.app.Application android.app.Application; android.os.Build android.os.Build; android.os.Handler android.os.Handler; android.os.StrictMode android.os.StrictMode;

public class ToDoApp extends Application { @Override public void onCreate() { super super.onCreate(); new Handler().postAtFrontOfQueue(this this::enableStrictMode); } private void enableStrictMode() { if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy(new new StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyDeath() .build()); } else { StrictMode.setThreadPolicy(new new StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyLog() .build()); }

357


INTEGRATING ROOM INTO THE REPOSITORY } } (from T30-RoomRepo/ToDo/app/src/main/java/com/commonsware/todo/ToDoApp.java)

onCreate()

is called when the ToDoApp instance is created, much like onCreate() of an activity. Ideally, we would just configure StrictMode there, but we have been advised by Google that this is not safe. Instead, we use a hacky bit of code — involving a Handler and postAtFrontOfQueue() – to arrange to get control again a bit later, after some more initialization work has been done in our process. We get that control in enableStrictMode(), courtesy of a Java 8 method reference in the postAtFrontOfQueue() method. In enableStrictMode(), we set up different StrictMode rules depending on whether this is a debuggable build or not. The BuildConfig class contains a series of code-generated values based on the settings that we have in Gradle and the manifest, and BuildConfig.DEBUG will be true for debuggable builds. The StrictMode configuration is mostly the same regardless of whether the app is debuggable or not: • Create a StrictMode.ThreadPolicy.Builder • Tell the Builder to detectAll() things that we might do wrong (versus a subset of StrictMode checks) • Configure the “penalty” that is to be applied if we do make a mistake • Build the StrictMode.ThreadPolicy and apply it as the policy for this process The difference between the two cases is that we use penaltyDeath() for debuggable builds and penaltyLog() for production builds. penaltyDeath() will crash the app if we do something wrong, which is nice and obvious in testing, but is rather userhostile for production. This will have no impact on our tests, as test methods are invoked on background threads automatically. But, if you use the app itself, everything should still work, as we have already added the logic in our RxJava and LiveData uses to have all of our I/ O be on background threads. StrictMode is just confirming that we did not make a mistake when doing any of that work.

358


INTEGRATING ROOM INTO THE REPOSITORY

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • • • • • • • • •

app/src/main/java/com/commonsware/todo/ToDoRepository.java app/src/main/java/com/commonsware/todo/Controller.java app/src/main/java/com/commonsware/todo/RosterViewModel.java app/src/androidTest/java/com/commonsware/todo/RepoTests.java app/src/androidTest/java/com/commonsware/todo/ControllerTest.java app/src/main/java/com/commonsware/todo/ToDoEntity.java app/build.gradle app/src/main/java/com/commonsware/todo/ToDoApp.java app/src/main/AndroidManifest.xml

359


Tracking Our Load Status

There are three logical states that our RosterListFragment and its RecyclerView can be in: • We have to-do items, and we are displaying them • We do not have to-do items, because the user has not entered any, and so we should show the “empty” view to help guide the user • We do not know whether we have to-do items or not, because we have not yet loaded them from the database That third state is not being handled by the app. Instead, we treat “do not know” as being the same as “we do not have to-do items” — we show the “empty” view if our RosterListAdapter is empty, no matter why it is empty. Plus, it would be nice to show some sort of “loading” indicator while the data load is in progress… such as a ProgressBar. So, in this tutorial, we will fix this. For most Android devices, and for shorter to-do lists, the difference will not be visible, as the data will load very rapidly. However, on slower devices, or with large to-do lists, the difference may be noticeable. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial](https://github.com/commonsguy/cwandexplore/tree/master/T31-Load/ToDo).

Step #1: Add an isLoaded() Property First, we need to track whether or not we have loaded our data. Since we need to render this information in the UI, part of the “state” tracked by the ViewState 361


TRACKING OUR LOAD STATUS should be a flag indicating whether we have loaded our data yet or not. Initially, that flag would indicate that the data is not yet loaded; we can then flip that flag when the data is loaded. First, add a new property (to be managed by AutoValue) to ViewState: public abstract boolean isLoaded(); (from T31-Load/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

Then, add the corresponding setter method to the ViewState.Builder class: abstract Builder isLoaded(boolean isLoaded); (from T31-Load/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

Next, modify toBuilder() to copy the isLoaded value from the current instance to the Builder, the way that we are copying the other properties: Builder toBuilder() { return builder().items(items()).current(current()).isLoaded(isLoaded()); } (from T31-Load/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

Finally, to initially set the value, modify builder() to set isLoaded to false: static Builder builder() { return new AutoValue_ViewState.Builder().isLoaded(false false); } (from T31-Load/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

Pre-populating a Builder is a common way with AutoValue to provide initial default values. In practice, probably the underlying boolean would be initialized to false, but this way, we are making that assignment explicit.

Step #2: Updating the Loaded Status Right now, that isLoaded value will always be false, since we never set it to true. Somewhere, as part of loading our data, we need to set it to true.

362


TRACKING OUR LOAD STATUS A likely place for that is where we are creating the post-load ViewState, in RosterViewModel. So, modify foldResultIntoState(), for the Result.Loaded branch, to set isLoaded to true: else if (result instanceof Result.Loaded) { List<ToDoModel> models=((Result.Loaded)result).models(); return ViewState.builder() .items(models) .current(models.size()==0 ? null : models.get(0)) .isLoaded(true true) .build(); } (from T31-Load/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

Step #3: Adjusting Our Layout We need to make a couple of changes to the layout used by RosterListFragment. Open res/layout/todo_roster.xml in the IDE. Click on the empty TextView in the â&#x20AC;&#x153;Component Treeâ&#x20AC;?. In the list of all attributes, find the visibility attribute, and set it to gone:

363


TRACKING OUR LOAD STATUS

Figure 183: Android Studio, Showing empty View Visibility Next, choose the “Widgets” category in the “Palette” view. You will see two labeled “ProgressBar”, one with a circle and one that is an actual bar:

Figure 184: Android Studio, Showing Palette Widgets Category

364


TRACKING OUR LOAD STATUS Typically, the circular ProgressBar is used for indefinite progress, where we do not know how long the work will take. The horizontal ProgressBar is more often used for cases where we can let the user know how far we have progressed. In this case, the work is fairly atomic: either our data is loaded or it is not. We have no intermediate steps with which to provide progress updates, so we should use the circular indefinite ProgressBar. However, we cannot drag and drop a widget into the preview area, since the preview is mostly our RecyclerView. The IDE will attempt to make our widget be a child of the RecylerView, and that does not work very well. Instead, drag the circular ProgressBar from the “Palette” and drop it on the ConstraintLayout entry in the “Component Tree” view. This will add it as a child to the ConstraintLayout, which is what we want. Then, use the grab handles on the ProgressBar to set up constraints to all four edges of the ConstraintLayout. However, there is a decent chance that you will sometimes get a constraint that ties the ProgressBar to the items RecyclerView, instead of to the parent ConstraintLayout, such as: <ProgressBar android:id="@+id/progressBar" style="?android:attr/progressBarStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" app:layout_constraintBottom_toBottomOf="@+id/items" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />

The simplest thing to do is to change the XML manually, so that all four constraints are set to parent: <ProgressBar android:id="@+id/progressBar" style="?android:attr/progressBarStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp"

365


TRACKING OUR LOAD STATUS android:layout_marginStart="8dp" android:layout_marginTop="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> (from T31-Load/ToDo/app/src/main/res/layout/todo_roster.xml)

Step #4: Reacting to the Loaded Status Finally, we need to use this isLoaded value in our UI. Specifically, we want: isLoaded

Value false true true

Number of Items in the Adapter any 0 >0

Empty View State

Progress View State

GONE

VISIBLE

VISIBLE

GONE

GONE

GONE

We will need access to our ProgressBar widget. So, add another View field for that to RosterListFragment, alongside the existing empty View: private View empty, progress; (from T31-Load/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

Initialize that field in onCreateView(): @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View result=inflater.inflate(R.layout.todo_roster, container, false false); rv=result.findViewById(R.id.items); empty=result.findViewById(R.id.empty); progress=result.findViewById(R.id.progressBar); return result; } (from T31-Load/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

366


TRACKING OUR LOAD STATUS Then, modify render() to implement the logic outlined in the above table: public void render(ViewState state) { adapter.setState(state); if (state.isLoaded()) { progress.setVisibility(View.GONE); if (adapter.getItemCount()==0) { empty.setVisibility(View.VISIBLE); } else { empty.setVisibility(View.GONE); } } else { progress.setVisibility(View.VISIBLE); empty.setVisibility(View.GONE); } } (from T31-Load/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

If you run the app, you may not see any effect, as the data will load too quickly. As an experiment, you could add an artificial delay to the data loading, by modifying foldResultIntoView() of RosterViewModel: else if (result instanceof Result.Loaded) { List<ToDoModel> models=((Result.Loaded)result).models(); android.os.SystemClock.sleep(2000); return ViewState.builder() .items(models) .current(models.size()==0 ? null : models.get(0)) .isLoaded(true true) .build(); }

Here, we use the SystemClock class and its sleep() method to add two seconds of delay to our background work of loading our data. If you run this version of the app, you will see the ProgressBar for those two seconds, before either the empty view appears or the list of to-do items appear:

367


TRACKING OUR LOAD STATUS

Figure 185: ToDo App, Showing ProgressBar However, be sure to remove that SystemClock.sleep() call after the experiment, so that you are not always waiting two seconds when you run the app.

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • • • •

app/src/main/java/com/commonsware/todo/ViewState.java app/src/main/java/com/commonsware/todo/RosterViewModel.java app/src/main/res/layout/todo_roster.xml app/src/main/java/com/commonsware/todo/RosterListFragment.java

368


Filtering Our Items

It is entirely possible that a user of this app will have a lot of to-do items. Rather than force the user to have to scroll through all of them in the list, we could offer some options for working with a subset of those items. In this tutorial, we will add a “filter” feature, to allow the user to work with either the outstanding to-do items, the completed items, or all of the items. This is a continuation of the work we did in the previous tutorial. The book’s GitHub repository contains the results of the previous tutorial as well as the results of completing the work in this tutorial.

Step #1: Adding a Checkable Submenu We have added quite a few action bar items in these tutorials. This time, we need to add one to allow the user to filter the list of items. To do that, we will use an action bar item that has a submenu of radio buttons, so the user can toggle between the different filter modes. But, first, we need another icon. Right-click over res/drawable/ in the project tree and choose “New” > “Vector Asset” from the context menu. This brings up the Vector Asset Wizard. There, click the “Icon” button and search for filter:

369


FILTERING OUR ITEMS

Figure 186: Android Studio Vector Asset Selector, Showing “filter” Options Choose the “filter list” icon and click “OK” to close up the icon selector. Then, click “Next” and “Finish” to close up the wizard and set up our icon. Next, open up the res/menu/actions_roster.xml resource file, and switch to the “Design” sub-tab. Drag an “Item” from the Palette view into the Component Tree, slotting it before the existing “add” item:

370


FILTERING OUR ITEMS

Figure 187: Android Studio Graphical Menu Editor, Showing New Item In the Attributes view for this new item, assign it an ID of filter. Then, choose both “ifRoom” and “withText” for the “showAsAction” option. Next, click on the “…” button next to the “icon” field. This will bring up an drawable resource selector. Click on ic_filter_list_black_24dp in the list of drawables, then click OK to accept that choice of icon. Then, click the “…” button next to the “title” field. As before, this brings up a string resource selector. Click on “Add new resource” > “New string Value” in the dropdown towards the top. In the dialog, fill in menu_filter as the resource name and “Filter” as the resource value. Click OK to close the dialog and complete the configuration of this action bar item:

371


FILTERING OUR ITEMS

Figure 188: Android Studio Menu Editor, Showing Configured MenuItem That gives the user something to click on, but now we need to set up a submenu. Unfortunately, at this point, the drag-and-drop functionality of the menu editor has a bug — we cannot create a submenu this way. Instead, switch over to the “Text” sub-tab and add a <menu> element as a child of the “filter” <item> element manually: <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> > <item android:id="@+id/filter" android:icon="@drawable/ic_filter_list_black_24dp" android:showAsAction="ifRoom|withText" android:title="@string/menu_filter"> > <menu> </menu> </item> <item android:id="@+id/add" android:icon="@drawable/ic_add_black_24dp"

372


FILTERING OUR ITEMS android:showAsAction="ifRoom|withText" android:title="@string/menu_add" /> </menu>

If you then switch back to the “Design” sub-tab, you should see the “menu” entry below our “filter” item in the Component Tree, though you may have to expand the tree yourself:

Figure 189: Android Studio Menu Editor, Showing New Submenu Then, from the Palette, drag a “Group” into the new “menu” in the Component Tree:

373


FILTERING OUR ITEMS

Figure 190: Android Studio Menu Editor, Showing New Group In the Attributes pane, give the group an ID of filter_group and set the “checkableBehavior” to “single”. Then, from the Palette, drag a “Menu Item” into the new group in the Component Tree:

374


FILTERING OUR ITEMS

Figure 191: Android Studio Menu Editor, Showing New MenuItem in the Group Drag two more “Menu Item” entries from the Palette and drop them in the group in the Component Tree, to give you a total of three items in the group. Select the first of the three submenu items in the Component Tree. In the Attributes pane, give it an ID of all. Switch the Attributes pane to show all attributes (if it is not doing so already), and check the “checked” checkbox, so that it contains a checkmark. Then, click the “…” button next to the “title” field. As before, this brings up a string resource selector. Click on “Add new resource” > “New string Value” in the drop-down towards the top. In the dialog, fill in menu_filter_all as the resource name and “All” as the resource value. Click OK to close the dialog and complete the configuration of this submenu item. Select the second submenu item in the Component Tree. In the Attributes pane, give it an ID of completed. Then, for the “title”, use the “…” button to assign it a new string resource, named menu_filter_completed, with a value of “Completed”. Select the third submenu item in the Component Tree. In the Attributes pane, give it an ID of outstanding. Then, for the “title”, use the “…” button to assign it a new string resource, named menu_filter_outstanding, with a value of “Outstanding”.

375


FILTERING OUR ITEMS At this point, in the â&#x20AC;&#x153;Textâ&#x20AC;? sub-tab, your menu resource XML should resemble: <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> > <item android:id="@+id/filter" android:icon="@drawable/ic_filter_list_black_24dp" android:showAsAction="ifRoom|withText" android:title="@string/menu_filter"> > <menu> <group android:id="@+id/filter_group" android:checkableBehavior="single"> > <item android:id="@+id/all" android:checked="true" android:title="@string/menu_filter_all" /> <item android:id="@+id/completed" android:title="@string/menu_filter_completed" /> <item android:id="@+id/outstanding" android:title="@string/menu_filter_outstanding" /> </group> </menu> </item> <item android:id="@+id/add" android:icon="@drawable/ic_add_black_24dp" android:showAsAction="ifRoom|withText" android:title="@string/menu_add" /> </menu> (from T32-Filter/ToDo/app/src/main/res/menu/actions_roster.xml)

And, if you run your app, you should see the new filter action bar item. Clicking it will expose the submenu, although clicking on the submenu items will have no effect.

376


FILTERING OUR ITEMS

Figure 192: ToDo App, Showing Checkable Submenu

Step #2: Getting Control on Filter Choices In particular, clicking on the submenu items does not even change their checked state. Even though our submenu looks like a group of radio buttons, it does not automatically behave like one. Instead, we need to add some code for that. In RosterListFragment, modify onOptionsItemSelected() to use a switch statement, and add case sections for our three new submenu items: @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.add: ((Contract)getActivity()).addModel(); return true true; case R.id.all: item.setChecked(true true); return true true; case R.id.completed:

377


FILTERING OUR ITEMS item.setChecked(true true); return true true; case R.id.outstanding: item.setChecked(true true); return true true; } return super super.onOptionsItemSelected(item); }

Here, we call setChecked(true) on the item parameter for the all, completed, and outstanding menu items. We do not need to worry about unchecking the others; that part of the typical radio button behavior is handled for us by the action bar. If you run the app, you will now see that clicking on those submenu items actually changes their checked state. The three case sections for our submenu items may seem redundant, since they all do the same thing. However, a bit later in this tutorial, we will need to emit new actions from our UI, and those actions will be tied to the new filter mode that the user asked for. That will require separate case sections, so we can set the proper filter mode.

Step #3: Defining a Filter Action and Result The user’s chosen filter mode should be part of our ViewState. Right now, we are not going to worry about saving it anywhere (such as to the database), but we can set ourselves up to support that in the future by having filter mode changes run through our MVI flow, the same as the other non-navigation aspects of user input in our app. For that, we will need to add new Filter and Action types, tied to filtering. However, first, we need a representation of the filter mode. Since we have three options (all, completed, and outstanding), a boolean will not work. So, let’s set up an enum for this. Right-click over the com.commonsware.todo package in the project tree, and choose “New” > “Java Class” from the context menu. Fill in FilterMode for the name, and choose “Enum” as the “Kind”. Then, click OK to create our empty FilterMode enum:

378


FILTERING OUR ITEMS package com.commonsware.todo; public enum FilterMode { }

Then, give it three values: ALL, COMPLETED, and OUTSTANDING: package com.commonsware.todo; public enum FilterMode { ALL, COMPLETED, OUTSTANDING } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/FilterMode.java)

Next, go into the Action class, and add a new Filter subclass: @AutoValue static abstract class Filter extends Action { public abstract FilterMode filterMode(); } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/Action.java)

Some of our other actions, like Show, wrap around a ToDoModel. In this case, Filter wraps around a FilterMode. Then, add a corresponding filter() method, matching the other static methods that we have on Action: public static Action filter(FilterMode mode) { return return(new new AutoValue_Action_Filter(mode)); } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/Action.java)

Next, go to the Result class, and add a new Result subclass: @AutoValue static abstract class Filtered extends Result { public abstract FilterMode filterMode(); } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/Result.java)

379


FILTERING OUR ITEMS Similarly, add a new filtered() method on Result: static Result filtered(FilterMode mode) { return return(new new AutoValue_Result_Filtered(mode)); } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/Result.java)

Step #4: Emitting and Controlling the Filter Action Given that we have our filter() method on Action, we can emit actions for our filter mode changes. With that in mind, modify onOptionsItemSelected() on RosterListFragment to call process() for our desired filter modes: @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.add: ((Contract)getActivity()).addModel(); return true true; case R.id.all: item.setChecked(true true); viewModel.process(Action.filter(FilterMode.ALL)); return true true; case R.id.completed: item.setChecked(true true); viewModel.process(Action.filter(FilterMode.COMPLETED)); return true true; case R.id.outstanding: item.setChecked(true true); viewModel.process(Action.filter(FilterMode.OUTSTANDING)); return true true; } return super super.onOptionsItemSelected(item); } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

Of course, right now, our action does not go anywhere, because the Controller does not know about it. So, add this method to Controller:

380


FILTERING OUR ITEMS private void filter(FilterMode mode) { resultSubject.onNext(Result.filtered(mode)); } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/Controller.java)

Also, modify processImpl() on Controller to call that new filter() method when it receives a Filter action: private void processImpl(Action action) { if (action instanceof Action.Add) { add(((Action.Add)action).model()); } else if (action instanceof Action.Edit) { modify(((Action.Edit)action).model()); } else if (action instanceof Action.Delete) { delete(((Action.Delete)action).model()); } else if (action instanceof Action.Load) { load(); } else if (action instanceof Action.Show) { show(((Action.Show)action).current()); } else if (action instanceof Action.Filter) { filter(((Action.Filter)action).filterMode()); } } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/Controller.java)

We can also test this logic. Add the following lines to the bottom of the controller() method in ControllerTest: actionSubject.onNext(Action.filter(FilterMode.OUTSTANDING)); Result.Filtered resultFiltered= (Result.Filtered)receivedResults.poll(1, TimeUnit.SECONDS); assertEquals(FilterMode.OUTSTANDING, resultFiltered.filterMode()); (from T32-Filter/ToDo/app/src/androidTest/java/com/commonsware/todo/ControllerTest.java)

If you run ControllerTest, the test should succeed.

381


FILTERING OUR ITEMS

Step #5: Updating the ViewState Now we need to use that Result.Filtered value, updating our ViewState to record the now-current FilterMode. We can start by teaching ViewState to hold onto a FilterMode. Add this abstract method to define a new filterMode() property for ViewState: public abstract FilterMode filterMode(); (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

Next, update the ViewState.Builder to have the corresponding setter: @AutoValue.Builder abstract static class Builder { abstract Builder items(List<ToDoModel> items); abstract Builder current(ToDoModel current); abstract Builder isLoaded(boolean isLoaded); abstract Builder filterMode(FilterMode filterMode); abstract ViewState build(); } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

In our menu resource, we pre-checked the all MenuItem. For consistency, we should start a ViewState the same way, with a default value of ALL for the FilterMode. To do that, modify the builder() method in ViewState to define that default: static Builder builder() { return new AutoValue_ViewState.Builder() .isLoaded(false false) .filterMode(FilterMode.ALL); } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

Then, modify toBuilder() in ViewState to copy the current filterMode() into the new Builder: Builder toBuilder() { return builder() .items(items()) .current(current())

382


FILTERING OUR ITEMS .isLoaded(isLoaded()) .filterMode(filterMode()); } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

The last change to ViewState is to give us a method to easily build a new ViewState with a revised FilterMode. Add this filter() method to ViewState: ViewState filter(FilterMode mode) { return return(toBuilder() .filterMode(mode) .build()); } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

Then, modify the foldResultIntoState() method in RosterViewModel to call that new filter() method when it encounters a Result.Filtered event: private ViewState foldResultIntoState(@NonNull ViewState state, @NonNull Result result) throws Exception { if (result instanceof Result.Added) { return state.add(((Result.Added)result).model()); } else if (result instanceof Result.Modified) { return state.modify(((Result.Modified)result).model()); } else if (result instanceof Result.Deleted) { return state.delete(((Result.Deleted)result).model()); } else if (result instanceof Result.Loaded) { List<ToDoModel> models=((Result.Loaded)result).models(); return ViewState.builder() .items(models) .current(models.size()==0 ? null : models.get(0)) .isLoaded(true true) .build(); } else if (result instanceof Result.Showed) { return state.show(((Result.Showed)result).current()); } else if (result instanceof Result.Filtered) { return state.filter(((Result.Filtered)result).filterMode()); } else { throw new IllegalStateException("Unexpected result type: "+result.toString()); } }

(from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/RosterViewModel.java)

383


FILTERING OUR ITEMS

Step #6: Filtering the Items Of course, through all of this, we have not actually filtered anything. We have simply gotten our FilterMode from our UI over to our ViewState to use for rendering purposes. So, we need a way to get the proper subset of the models to use to populate our RecyclerView. First, letâ&#x20AC;&#x2122;s write the actual filtering algorithm. Just as ToDoModel knows how to compare two models (via its SORT_BY_DESC Comparator), ToDoModel should know how to filter a list of models. To that end, add this method to ToDoModel: public static List<ToDoModel> filter(List<ToDoModel> models, FilterMode filterMode) { List<ToDoModel> result; if (filterMode==FilterMode.COMPLETED) { result=new new ArrayList<>(); for (ToDoModel model : models) { if (model.isCompleted()) { result.add(model); } } } else if (filterMode==FilterMode.OUTSTANDING) { result=new new ArrayList<>(); for (ToDoModel model : models) { if (!model.isCompleted()) { result.add(model); } } } else { result=new new ArrayList<>(models); } return Collections.unmodifiableList(result); } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/ToDoModel.java)

If the FilterMode is ALL, we just create a new ArrayList with all of the supplied models. Otherwise, we iterate over the supplied models and only put the proper ones into the result. 384


FILTERING OUR ITEMS Next, add this method to ViewState: @Memoized public List<ToDoModel> filteredItems() { return return(ToDoModel.filter(items(), filterMode())); } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/ViewState.java)

AutoValue offers @Memoized. This annotation teaches AutoValue to override our filteredItems() method in subclasses, caching the result there. So the first time something calls filteredItems(), we compute the filtered roster for this ViewState. Since both the items() and the filterMode() are immutable, we do not need to recalculate the filteredItems() every time, so @Memoized caches the first callâ&#x20AC;&#x2122;s result for us, supplying the cached value for future calls to filteredItems(). Now, our fragments have the option of calling items() to get all of the models or filteredItems() to get the filtered subset for the current FilterMode.

Step #7: Using the Filtered Items Right now, we use items() in one place: the setState() method in RosterListAdapter: void setState(ViewState state) { models=state.items(); notifyDataSetChanged(); }

Change that to use filteredItems(): void setState(ViewState state) { models=state.filteredItems(); notifyDataSetChanged(); } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/RosterListAdapter.java)

If you run the app, you can apply the filter via the action bar item, toggling between the three filter modes, seeing the filtered results in the list.

385


FILTERING OUR ITEMS

Step #8: Fixing the Empty Text At this point, there are two situations when we have an empty list: 1. If there are no to-do items at all 2. If there are no to-do items in the current filter mode (e.g., all of the items are outstanding, and the filter mode is set to COMPLETED) We should improve this. First, go into res/values/strings.xml and add a new string resource: <string name="msg_empty_filtered"> >Click the + icon to add a todo item, or change your filter to show other items</string> </string>

(from T32-Filter/ToDo/app/src/main/res/values/strings.xml)

(note: this will be shown in the book as split across multiple lines, but you are welcome to have it be all on one line in your project, if you wish) Next, open res/layout/todo_roster.xml in the IDE. Click on our empty TextView. Switch the Attributes pane to the full list and fold open the â&#x20AC;&#x153;Paddingâ&#x20AC;? options:

386


FILTERING OUR ITEMS

Figure 193: Android Studio, Showing TextView Padding Options Click the “…” button next to the “all” entry in the “Padding” options. In there, create a new dimension resource, named empty_padding, with a value of 8dp:

387


FILTERING OUR ITEMS

Figure 194: Android Studio, Showing New Dimension Resource Value Dialog Click OK to close up the dialogs and assign that dimension resource to the padding for all four sides. This will prevent our empty message from running all the way to the edges of the screen. Then, scroll down in the Attributes pane and fold open the â&#x20AC;&#x153;gravityâ&#x20AC;? options:

388


FILTERING OUR ITEMS

Figure 195: Android Studio, Showing TextView Gravity Options Check the “center” option, which will cause our text to be centered within the space being occupied by the TextView. Selecting “center” will also cause “center_horizontal” and “center_vertical” to be checked with gray checkmarks:

389


FILTERING OUR ITEMS

Figure 196: Android Studio, Showing TextView Checked Gravity Options In RosterListFragment, we have access to the empty view, but we have the field declared as a View. We are going to modify the text in that view, and for that, we need the field to be a TextView. So, change the empty field to be a TextView, while leaving progress as a plain View: private View progress; private TextView empty; (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

Finally, replace the current render() method in RosterListFragment to be: public void render(ViewState state) { adapter.setState(state); if (state.isLoaded()) { progress.setVisibility(View.GONE); if (state.items().size()==0) { empty.setVisibility(View.VISIBLE); empty.setText(R.string.msg_empty);

390


FILTERING OUR ITEMS } else if (state.filteredItems().size()==0) { empty.setVisibility(View.VISIBLE); empty.setText(R.string.msg_empty_filtered); } else { empty.setVisibility(View.GONE); } } else { progress.setVisibility(View.VISIBLE); empty.setVisibility(View.GONE); } } (from T32-Filter/ToDo/app/src/main/java/com/commonsware/todo/RosterListFragment.java)

If our state is loaded, we check to see if we have any items. If not, we show the empty view, with the text set to be the original msg_empty string. If we have items, but the filteredItems() list is empty, we also show the empty view, but we set the text to our new msg_empty_filtered string. If we have both items and filtered items, we hide the empty view, as our RecyclerView will be showing those filtered items. Now, if you run the app, you will see the empty message centered, and you will see the new empty message if you have items but they are all hidden by the filter:

391


FILTERING OUR ITEMS

Figure 197: ToDo App, Showing Revised Empty Message

What We Changed The book’s GitHub repository contains the entire result of having completed this tutorial. In particular, it contains the changed files: • • • • • • • • • • • • • •

app/src/main/res/drawable/ic_filter_list_black_24dp.xml app/src/main/res/menu/actions_roster.xml app/src/main/res/values/strings.xml app/src/main/java/com/commonsware/todo/RosterListFragment.java app/src/main/java/com/commonsware/todo/FilterMode.java app/src/main/java/com/commonsware/todo/Action.java app/src/main/java/com/commonsware/todo/Result.java app/src/main/java/com/commonsware/todo/Controller.java app/src/androidTest/java/com/commonsware/todo/ControllerTest.java app/src/main/java/com/commonsware/todo/ViewState.java app/src/main/java/com/commonsware/todo/RosterViewModel.java app/src/main/java/com/commonsware/todo/ToDoModel.java app/src/main/java/com/commonsware/todo/RosterListAdapter.java app/src/main/res/layout/todo_roster.xml

392

Exploring Android  
Exploring Android  
Advertisement