Android code clean-up: concatenating strings with setText

The Android Studio code inspector is a useful tool for maturing your code and improving your style.

Android code clean-up: concatenating strings with setText

Android Studio is based on Jetbrain's IntelliJ and, along with most (all?) products based on IntelliJ features code inspection and clean-up tools.  These are really useful tools that can help find problems, fix bugs, and improve your style.  In this post I'm going to look at the warning the inspector gives for concatenating strings with setText.

What's concatenation?

To concatenate is to put two or more things together.  For example, consider three text stings: apple, cat, and wallet.  A simple concatenation of those three strings would get us applecatwallet.  How to concatenate depends on your programming language and I've put some examples below:

// PHP
echo 'apple'.'cat'.'wallet';

// Or, using variables:
$string1 = 'apple';
$string2 = 'cat';
$string3 = 'wallet';

echo $string1.$string2.$string3;
Concatenating in PHP
// Java
System.out.println("apple"+"cat"+"wallet");
Concatenating in Java
Screenshot showing a spreadsheet with apple, cat and wallet in column A and the concatenate function in B2.
Concatenate function in Google Sheets

Concatenating with setText

Running the inspector on eVitabu I found a number of places that we were concatenating strings from our translations with other data.  This can be a problem where word order changes in languages and the inspector shows the following guidance:

Do not concatenate text displayed with setText.  Use resource string with placeholders.

Which is shown in the results like this:

Screenshot of Android Studio's inspection result, highlighting the offending code.

The offending code is highlighted by Android Studio and I've shown it below:

language.setText(activity.getString(R.string.language) + ": " + item.getLanguage());

What the above code is doing is taking the language object (in this case an element that's displayed in the Android app) and setting the text that will be displayed.  There's three parts to this - first the localised word for "Language", followed by ": " (colon space) and then value of item.getLanguage() which finds the language of a piece of content.  Once the concatenation is complete the results are displayed in a pop-up dialog about the content, like this (I've highlighted the element the above code worked on):

Screenshot showing a dialog detailing properties of a piece of content, like author and language.  Language is highlighted.
The code above concatenated the Language element.

You may be thinking the above is a pretty tame example, and it is, but I'm a firm believer in doing things the right way where possible so let's sort this out!

How do we fix this?

Resolving this issue is a two step process.  Firstly we have to modify our strings for each translation to have a placeholder (or as many as we need) at the right place.  For the example above that means taking the original string:

    <string name="language">Language</string>

And making it:

    <string name="language">Language: %1$s</string>

You'll note that I've added the ": " previously added by concatenation in Java and that I've also added %1$s which is our placeholder.  In this case %1 means "the first argument" and $s means "of type string" and I'll explain how that's used below.  If you're concatenating with a number you'd use $d instead.

Refactor your Java

Once you've updated your string it's time to refactor your code.  The original code, including declaring the language object, was:

TextView language = findViewById(R.id.txt_language);
language.setText(activity.getString(R.string.language) + ": " + item.getLanguage());

So we make it:

TextView language = findViewById(R.id.txt_language);
String languageText = activity.getString(R.string.language, item.getLanguage());
language.setText(languageText);

It's not immediately clear what's happening here, so let's unpack it.  We declare the language object as being an Android view element that we find by ID (R.id.txt_language):

TextView language = findViewById(R.id.txt_language);

Next we set the string value that we'll use later to apply to the language object.  The first argument of the getString() method is mandatory and is the text to look up - in this case the language element of translation file (R.string.language).  Additional, optional, arguments can be passed to getString, separated by commas (,) and the placeholders are replaced by these.  In this case we pass item.getLanguage() as the argument:

String languageText = activity.getString(R.string.language, item.getLanguage());
Performing the replacement.

Finally we set the Android view element to have the value we've just built:

language.setText(languageText);

After we've done that the inspection results will update to show our fix has applied and we can move onto the next one:

Screenshot showing the inspection result has been marked as done ("no longer valid").
Note the result now shows "no longer valid".

Multiple placeholders

You can use multiple placeholders in your strings, even mixing text and numbers.  Simply indicate in the string which argument should be placed into the placeholder (so %2 for the second, %6 for the sixth etc.) and pass the relevant number of arguments:

    <string name="wordwordnumber">I eat %1$s and %2$s %3$d times a day.</string>
String eatingText = activity.getString(R.string.wordwordnumber, "apples", "pears", 3);

A note on refactoring order

If you refactor your Java before your strings AndroidStudio will alert you to the fact that you can't perform a substitution.  I found this out when I tried to refactor all my Java in one go and sadly the error message isn't as clear as it could be:

Format string 'resourceSeries' is not a valid format string so it should not be passed to String.format

This error will persist until you've added your placeholders to the string resource.  Scarily this looks very red in Android Studio:

Screenshot showing AndroidStudio highlighting my code is wrong, using the scary shade of red!

Conclusion

While the examples presented here are minor transgressions in my view, there's something satisfying about knocking some easily fixed issues on the head.  It's worth noting that you probably won't ever get the inspector to be 100% happy unless you really aim for that, however, that's probably not worth your effort.


Banner image: Screenshot of the code inspector results.