Defining string packs with different content but same structure in android resources

Viewed 121

I have a game where I have a hierarchy where a season has multiple levels and a level has multiple tasks.

Now, I have an Activity that shows a congratulatory message for each of these parts of the hierarchy which are divided in different strings. Each of them have:

  • Exclamation
  • Header
  • Description
  • Continue Message

For example season:

  • "HURRAY!"
  • "Season Finished"
  • "You have successfully finished season X"
  • "Continue to season X+1"

My first idea was to create an enum that holds the appropriate values. Like this:

enum class CongratulationsType(val exclamation: String, val header: String) {
    SEASON("HURRAY!", "You finished season %d!!"),
    LEVEL("YAY!", "Well done! Level over!"),
    TASK("NICE!", "This looked too easy")
}
...
val headerString = CongratulationsType.SEASON.header

But strings should be saved in resources. So I would have to put the strings into the resources and correctly link the resources to the enum values.

This would mean that I kinda do the work twice. I would define the strings in the resource file without a "binding structure", just to then put them into the enum to have a "binding structure". By "binding structure" I mean the structure described above where it's mandatory for the specific fields to exist and where I would be able to call something like CongratulationsType.SEASON.header.

My question is: Is it possible to "pack" strings in some defined structure and have that structure be something like an enum or a hashmap? Something like:

Pack definition

<resources>
   <pack_def name="congrats">
      <string name="exclamation"/>
      <string name="header"/>
      ...
   </pack_def>
<resources>

Pack implementation

<resources>
   <pack name="season" type="congrats">
      <string name="exclamation">HURRAY!</string>
      <string name="header">Season Finished</string>
      ...
   </pack>
<resources>

Usage

// In code
//// function definition
fun setMessage(congrats: Congrats) {
    exclamationTextView.text = congratsType.exclamation
    headerTextView.text = congratsType.header
    ...
}
    
//// function use
setMessage(Congrats.Season)

// In Resources
<TextView
    android:text="@packs/congrats/season/exclamation"
/>

I know that I could use string array resources but they seem a little bit "unclean" because, as I said, I want the structure to be "binding" and called like enums. I'm specifically asking for a way to do it as close to enums as possible without having to assign a resource to each field for each enum value.

2 Answers

My question is: Is it possible to "pack" strings in some defined structure and have that structure be something like an enum or a hashmap? Something like:

android:text="@packs/congrats/season/exclamation"

Short answer - no. Android will not know how to parse and extract values from these new structures.

What you can do:

  1. Create 4 string resource arrays: Exclamations, Headers, Descriptions, Continue Messages;
  2. Create kotlin data class with 4 string variables. Let's call it Congratulation;
  3. Map resources to an array with of Congratulations.

Example:

In strings.xml file you declare next arrays:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="exclamations">
        <item>Exclamation 1!!!</item>
        <item>Exclamation 2!!!</item>
        ...
        <item>Exclamation N!!!</item>
    </string-array>

    <string-array name="headers">
        <item>Header 1</item>
        <item>Header 2</item>
        ...
        <item>Header N</item>
    </string-array>
    
    <!-- etc. -->
</resources>

Or you can use already defined resources in these arrays just to order them:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="exclamations">
        <item>@string/exclamation_1</item>
        <item>@string/exclamation_2</item>
        ...
        <item>@string/exclamation_n</item>
    </string-array>

    <!-- etc. -->
</resources>

Define data class:

data class Congratulation(val exclamation: String, val header: String, 
                          val description: String, val continueMessage: String)

Map resources:

val exclamations = context.resources.getStringArray(R.array.exclamations)
val headers = context.resources.getStringArray(R.array.headers)
val descriptions = context.resources.getStringArray(R.array.descriptions)
val continueMessages = context.resources.getStringArray(R.array.continue_messages)

val congratulations = exclamations.mapIndexed { i, exclamation ->
    Congratulation(exclamation, headers[i], descriptions[i], continueMessages[i])
}

Pros

  • No need to mention each string resource in code, only 4 array resources;
  • Following the first point, each time you add new value you don't need to modify your code. You can initialize this array at the moment of application launch once and use later.

Cons

  • Order of translations must be precise! If you accidentally change order within <string-array> - no warning will appear. You will have to check the order manually or create specific tests;
  • If accidentally you add fewer entries to any of the headers, descriptions or continueMessages array - the app will crash with IndexOutOfBoundsException. Although, it will crash as soon as you try to map values and it is easy to debug.
  • Using when statements becomes impossible.

It's not exactly what you're asking for, but just in case the only things changing in your messages are the season numbers, you can use format strings and pass in the appropriate numbers when you display the messages:

<string name="continue_message">Continue to season %d</string>

continueTextView.text = getString(R.string.continue_message, season+1)

If you do want to be able to set arbitrary strings for each season or whatever, Jenea has it right I think - there's no way to enforce a structure in the strings XML like that (I'm not totally familiar with the XML format in general, you can do some fancy things with it but I think they're limited in what Android will actually interpret and use). You can do runtime validation (maybe only in debug builds, with a thorough check across all locales) but you have to design things so you catch any problems that might occur


edit just to make it clear what I mean about the templates

If you effectively have 4 strings, it's just that you have lots of variations depending on things like season number, level number etc, you might be able to use format strings so you you only need exactly 4 in your strings file:

<string name="season_exclamation">HURRAY!</string>
<string name="level_exclamation">YAY!</string>
<string name="season_header">You finished season %d!!</string>
<string name="level_header">Well done! Level over!</string>

That way you're enforcing a structure, there's no variable length arrays or anything - you have exactly 4 string resources, with 4 specific IDs (and if you end up localising them, it will warn you if you haven't provided them all).

So you can easily make an enum, it's just that your strings are defined in a different place

enum class Congratulations(
    @StringRes private val headerResId: Int,
    @StringRes private val exclamationResId: Int
) {
    SEASON(R.string.season_header, R.string.season_exclamation),
    LEVEL(R.string.level_header, R.string.level_exclamation);

    fun getHeader(context: Context) = context.getString(headerResId)
    // etc
}

The only tricky thing is you need to pass in that context to pull out the appropriate string resources, instead of just declaring string literals. If you want, you could set a Context on the class (early, before you access any strings) and do something like this

enum class Congratulations(
    ...
    ...
    companion object {
        lateinit var context: Context
    }

    val header: String get() = context.getString(R.string.header)
    // or, so you only grab the strings once
    val exclamation: String by lazy { context.getString(R.string.exclamation) }
}

The point about the format strings is that you don't need to create separate string resources for "bla bla season 1" and "bla bla season 2" and constantly add to the strings file, if you can use of a template string you could have a function on your enum like fun getContinueMessage(seasonNumber): String and then you don't need SEASON1, SEASON2, SEASON3 enums. Although at that point, you're better off using a sealed class since not all your Congratulation types will need that, and sealed classes will let you include different objects instead of an enum where everything is the exact same type

Related