Creating hashmap/map from XML resources

XmlAndroid

Xml Problem Overview


I'm making an application where a web service fetches (amongst other) a bunch of codes from a webservice (I.e BEL, FRA, SWE). During runtime I want to translate these codes to their apporiate names to display to users (I.e Belgium, France, Sweden). There can be a lot of these codes, so i'm wondering if there is any approriate way to store the (code, name) entry as a some sort of map in the XML resources in Android, so I can quickly fetch the name by the given code?

It's all about speed here, since the map can have a few hundred entries.

Xml Solutions


Solution 1 - Xml

Also you can define a map in XML, put it in res/xml and parse to HashMap (suggested in this post). If you want to keep key order parse to LinkedHashMap. Simple implementation follows:

Map resource in res/xml:

<?xml version="1.0" encoding="utf-8"?>
<map linked="true">
    <entry key="key1">value1</entry>
    <entry key="key2">value2</entry>
    <entry key="key3">value3</entry>
</map>

Resource parser:

public class ResourceUtils {
	public static Map<String,String> getHashMapResource(Context c, int hashMapResId) {
		Map<String,String> map = null;
		XmlResourceParser parser = c.getResources().getXml(hashMapResId);
		
		String key = null, value = null;
		
		try {
			int eventType = parser.getEventType();

			while (eventType != XmlPullParser.END_DOCUMENT) {
				if (eventType == XmlPullParser.START_DOCUMENT) {
					Log.d("utils","Start document");
				} else if (eventType == XmlPullParser.START_TAG) {
					if (parser.getName().equals("map")) {
						boolean isLinked = parser.getAttributeBooleanValue(null, "linked", false);
						
						map = isLinked ? new LinkedHashMap<String, String>() : new HashMap<String, String>();
					} else if (parser.getName().equals("entry")) {
						key = parser.getAttributeValue(null, "key");
						
						if (null == key) {
							parser.close();
							return null;
						}
					}
				} else if (eventType == XmlPullParser.END_TAG) {
					if (parser.getName().equals("entry")) {
						map.put(key, value);
						key = null;
						value = null;
					}
				} else if (eventType == XmlPullParser.TEXT) {
					if (null != key) {
						value = parser.getText();
					}
				}
				eventType = parser.next();
			}
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}

		return map;
	}
}

Solution 2 - Xml

Today I came across the same problem and studying developer.android.com for a long time didn't help since Android resources cannot be hashes (maps), only arrays.

So I found 2 ways:

  1. Have a string array of values like "BEL|Belgium", parse those string early in the program and store in a Map<>

  2. Have 2 string arrays: first with the values of "BEL", "FRA", "SWE" and second with "Belgium", "France", "Sweden".

Second is more sensitive cause you have to synchronize changes and order in both arrays simultaneously.

Solution 3 - Xml

I've found Vladimir's answer quite compelling so I implemented his first suggestion:

public SparseArray<String> parseStringArray(int stringArrayResourceId) {
    String[] stringArray = getResources().getStringArray(stringArrayResourceId);
    SparseArray<String> outputArray = new SparseArray<String>(stringArray.length);
    for (String entry : stringArray) {
        String[] splitResult = entry.split("\\|", 2);
        outputArray.put(Integer.valueOf(splitResult[0]), splitResult[1]);
    }
    return outputArray;
}

using it is straight forward. In your strings.xml you have a string-array like so:

<string-array name="my_string_array">
    <item>0|Item 1</item>
    <item>1|Item 4</item>
    <item>2|Item 3</item>
    <item>3|Item 42</item>
    <item>4|Item 17</item>
</string-array>

and in your implementation:

SparseArray<String> myStringArray = parseStringArray(R.array.my_string_array);

Solution 4 - Xml

I had a similar requirement, and I solved it in a way that is more powerful than the current solutions.

First create a resource that is an array of country arrays:

<array name="countries_array_of_arrays">
    <item>@array/es</item>
    <item>@array/it</item>
</array>

Then for each country add an array with the country info:

<array name="es">
    <item>ES</item>
    <item>ESPAÑA</item>
</array>
<array name="it">
    <item>IT</item>
    <item>ITALIA</item>
</array>

Some notes:

  1. The name values (es, it) on each country array must match the ones in countries_array_of_arrays (you'll get an error if it doesn't).

  2. The order of the items on each array must be the same for all the arrays. So always put the country code first, and the country name second, for example.

  3. You can have any number of strings for each country, not just the country code and name.

To load the strings on your code do this:

TypedArray countriesArrayOfArrays = getResources().obtainTypedArray(R.array.countries_array_of_arrays);
int size = countriesArrayOfArrays.length();
List<String> countryCodes = new ArrayList<>(size);
List<String> countryNames = new ArrayList<>(size);
TypedArray countryTypedArray = null;
for (int i = 0; i < size; i++) {
    int id = countriesArrayOfArrays.getResourceId(i, -1);
    if (id == -1) {
        throw new IllegalStateException("R.array.countries_array_of_arrays is not valid");
    }
    countryTypedArray = getResources().obtainTypedArray(id);
    countryCodes.add(countryTypedArray.getString(0));
    //noinspection ResourceType
    countryNames.add(countryTypedArray.getString(1));
}
if (countryTypedArray != null) {
    countryTypedArray.recycle();
}
countriesArrayOfArrays.recycle();

Here I'm loading the resources to 2 List but you can use a Map if you want.

Why is this more powerful?

  1. It allows you to use string resources and hence translate the country names for different languages.

  2. It allows you associate any type of resource to each country, not just strings. For example, you can associate a @drawable with a country flag or an <string-array> that contains provinces/states for the country.

Advanced example:

<array name="it">
    <item>IT</item>
    <item>ITALIA</item>
    <item>@drawable/flag_italy</item>
    <item>@array/provinces_italy</item>
</array>

Note that I haven't tried this but I've seen it in other questions. In such case you would use countryTypedArray.getDrawable(2) to get the drawable, for example.

Simplification:

If you are only using strings for the country array, you can use an <string-array (instead of an <array>) like this:

<string-array name="it">
    <item>IT</item>
    <item>ITALIA</item>
</string-array>

And you can simplify a bit the for-loop code by directly getting an String[] instead of a TypedArray:

String[] countryArray = getResources().getStringArray(id);
countryCodes.add(countryArray[0]);
countryNames.add(countryArray[1]);

A note on having 2 <string-array>:

Initially I was thinking of having 2 <string-array> (one for the country code and another for the country name) as mentioned on another answer. However, with 2 <string-array> you have to make sure that the number and order of the items match. With this solution you don't. It's safer in this regard. Note that, as I've said, the order of the items on each country array matters: always put the country code and country name in the same order!

Solution 5 - Xml

I think the safest and cleanest approach hasn't been posted yet, so here's my $0.02:

How about creating an enum type in the code that references the string resource id for its display value:

public enum Country {
   FRA(R.string.france),
   BEL(R.string.belgium),
   SWE(R.string.sweden),
   ...;

   @StringRes public int stringResId;

   Country(@StringRes id stringResId) {
       this.stringResId = stringResId;
   }
} 

Then in strings.xml:

<string name="france">France</string>
...

And when you receive an item from the web service:

Country country = Country.valueOf(countryCodeFromServer);
context.getResources().getString(country.stringResId);

Build time checks will ensure that each country has a resource for each locale, which you won't have if you maintain the list as an array resource.

Solution 6 - Xml

It might be late for you, but for anyone else interested. If your 3 digit country codes are valid ISO 3166-1 alpha-3 codes

Locale locale = new Locale("", "SWE");
String countryName = locale.getDisplayCountry();

This gives full country name with correct language.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKimHafrView Question on Stackoverflow
Solution 1 - XmlvokilamView Answer on Stackoverflow
Solution 2 - XmlVladimirView Answer on Stackoverflow
Solution 3 - XmlJens KohlView Answer on Stackoverflow
Solution 4 - XmlAlbert Vila CalvoView Answer on Stackoverflow
Solution 5 - XmlJohnny CView Answer on Stackoverflow
Solution 6 - Xmluser1435828View Answer on Stackoverflow