This summer I have been adding some features to Nurse AMIE, an Android app proposed, developed, and tested by Dr. Kathryn Schmitz at the Penn State Cancer Institute.
In the last post about this app, I talked to you about the role of empathy in mobile apps for care management. We discussed the user interface design decisions that went into the weekly survey form, and why they mattered. In this post, I’ll talk about the technical execution of the form.
To help you understand my decisions about this form, you need to see the questions:
- What was the severity of your decreased appetite at its worst?
- How much did decreased appetite interfere with your usual or daily activities?
- How often did you have nausea?
- What was the severity of your nausea at its worst?
- How often did you have vomiting?
- What was the severity of your vomiting at its worst?
- What was the severity of your constipation at its worst?
- How often did you have loose or watery stools (diarrhea/diarrhoea)?
- How often did you have pain in the abdomen (belly area)?
- What was the severity of your pain in the abdomen (belly area) at its worst?
- How much did pain in the abdomen (belly area) interfere with your usual or daily activities?
- What was the severity of your shortness of breath at its worst?
- How much did your shortness of breath interfere with your usual or daily activities?
- What was the severity of your cough at its worst?
- How much did cough interfere with your usual or daily activities?
- What was the severity of your numbness or tingling in your hands or feet at its worst?
- How much did numbness or tingling in your hands or feet interfere with your usual or daily activities?
- How often did you have pain?
- What was the severity of your pain at its worst?
- How much did pain interfere with your usual or daily activities?
- What was the severity of your fatigue, tiredness, or lack of energy at its worst?
- How much did fatigue, tiredness, or lack of energy interfere with your usual or daily activities?
- What was the severity of your pain or burning with urination at its worst?
- How often did you have hot flashes/flushes?
- What was the severity of your hot flashes/flushes at their worst?
Each question had a set of radio button responses, like so:
Many of these questions are similar, but not exactly the same. I’ll show you where I placed my seams, and you can choose whether you agree with my choices or not.
Among these 25 questions, I notice that each one takes two categories.
- A symptom. Each question asks about one of thirteen symptoms: decreased appetite, nausea, vomiting, constipation, diarrhea, abdominal pain, shortness of breath, cough, numbness/tingling, general pain, fatigue, urination pain, and hot flashes/flushes.
- A modifier. Each question asks about one of three modifiers: frequency, severity, and interference in daily life. Some symptoms have questions for all three of these modifiers, but most only have questions for one or two.
We’ll want our view to divide up the survey into sections for each symptom. The real potential for duplication in this code, though, lies in the radio buttons. We have 25 questions that fall into three modifier categories, and for each of those modifier categories, the radio buttons have the same labels:
- frequency: never, rarely, occasionally, frequently, almost constantly
- severity: none, mild, moderate, severe, very severe
- interference: not at all, a little bit, somewhat, quite a bit, very much
Reusing Views in Android
Android allows us to extract our own layout XML files and then include them in other layout files like this:
In this case, the layout in
fragment_weekly_survey.xml includes a custom view that lives in an xml file called
There are some weird rules about how these custom layouts work. First of all, I can assign an id to my custom view (see line 3), but it only gets registered if I also assign the layout_width and layout_height (see lines 4 and 5). Also, if I want to be able to assign an id, then my custom layout has to contain a single child, and that child has to be of the layout type, like so:
Android developers the world over cringe at this because
fragment_weekly_survey.xml already has a base layout, meaning that this custom view nests a layout inside a layout, and the deeper you go doing that, the longer it takes Android to render the screen. (Luckily, just one layer doesn’t usually produce a noticeable difference). Moreover, in this case, the weekly survey fragment’s base layout is already a LinearLayout that stacks views up on top of each other without overlapping them—so the additional LinearLayout in the custom view doesn’t provide any new information about how the views should be arranged.
Android does have a way around this, called the
<merge> tag. I could put the custom view inside a
<merge> tag, and this will tell Android to drop all the views inside it wholesale into the parent layout, without requiring their own layout. I tried it on this app, but it turns out
<merge> tags do not play nice with assigning an id to the custom views, and I need to do that to be able to tell which radio group is answering which question in the fragment.
onResume() method of my Fragment, where you can see the view binding:
Yikes. It’s long and repetitive.
Theoretically we could do some fancy meta-programming here with string evaluation of the view ids to collect all the question responses, but I didn’t think that length alone was sufficient justification to do something that clever instead of leaving it clear what’s happening. It’s also worth noting that lines 7-31 would be unnecessary with view binding as long as the variables had the same names as the view IDs (or even, it turns out, camelCase versions of equivalent snake_case_view_ids). Unfortunately, view binding does not play nice with the “include” syntax, and in the process of trying it on this code base I learned that the generated view binding classes look like this:
So they’re not producing a more efficient pattern; they’re just generating boilerplate that devs were writing manually*. That’s minimally helpful. It would edge over into helpful if it worked seamlessly, but it doesn’t:
What’s happening here, I believe, is that the generated class needs to be explicitly casting those view assignments with
findViewById<ViewType> rather than straight
findViewById. At this point, I’d rather spend three minutes cranking boilerplate that I know is going to work than patch generated code.
*View binding does add null safety, which
findViewById lacks. This is, theoretically, its big selling point. Guess what, I found the special sauce, and it’s this:
A @NonNull annotation. This is still not, to me, edging out functioning boilerplate by very much.
Lines 35-60 tie each response view to a name that we use to refer to the question that the response answers. Then on lines 72-76, during form submission, we map each of those key-value pairs to a new key-value pair, with the same key as the original pair, pointing to the string label of the radio button that is filled in for that question. There will be no nulls here: instead of introducing logic to handle null, we set a default value of
"UNKNOWN" for unanswered questions. As it so happens, we also insist that all questions be answered before letting someone submit this form, but even if we didn’t do that, our app would not crash over a null pointer exception here.
This is the method that extracts the responses, as strings, from our custom response views (and sets the form to valid or invalid based on whether all questions have an answer):
This code also reveals the purpose of the
surveyResponses hash that we initialize in
onResume(): on lines 7 and 11, we manipulate the background color of the view in that hash’s value to help the person easily see which responses still need to be filled in when they try to submit without completing the whole form.
To dupe…or not to dupe?
I did not extricate custom views for the collection of section headers, or for the questions themselves. There is some duplicate wording in those questions that therefore propagates to the code. I’m okay with this. In my experience, DRY-ing up text (and code, for that matter), with subtle differences is a fun thought experiment that doesn’t fare as well on a cost-benefit analysis as we programmers tend to assume it does.
So the XML that renders the hot flashes/flushes question that you see above lives in the
fragment_weekly_survey.xml file, and it looks like this:
I could theoretically use data binding to consolidate common view patterns a little more. I struggled with this for a while, but I could not find a solution to, for example, make the TextViews above an included question_view and pass in a text attribute.
I’d love a better way to label and handle the responses such that adding and removing questions requires changes in fewer places. That’s my personal aesthetic preference, though: I don’t have any evidence so far that we’ll need to do that.
And so…ta-da! Our view. In the next post on this application, I’ll talk about how we persist this weekly survey data to a local database.
If you liked this piece, you might also like:
The behind the scenes series