24

I was trying to define a static hash table that makes use of resources, but I got stonewalled by the impossibility of accessing resources statically.

Then I realized that the best of all places to define a static map is in the resources files themselves.

How can I define a map in XML? I believe that if possible it should be similar to the Listpreference mechanism, with entries and entries-values.

ilomambo
  • 8,290
  • 12
  • 57
  • 106

5 Answers5

42

A simpler option would be to use two arrays. This has the benefit of not iterating the XML file again, uses less code, and is more straightforward to use arrays of different types.

<string-array name="myvariablename_keys">
   <item>key1</item>
   <item>key1</item>
</string-array>


<string-array name="myvariablename_values">
   <item>value1</item>
   <item>value2</item>
</string-array>

Then your java code would look like this:

String[] keys = this.getResources().getStringArray(R.array.myvariablename_keys);
String[] values = this.getResources().getStringArray(R.array.myvariablename_values);
LinkedHashMap<String,String> map = new LinkedHashMap<String,String>();
for (int i = 0; i < Math.min(keys.length, values.length); ++i) {
   map.put(keys[i], values[i]);
}
YaMiN
  • 3,194
  • 2
  • 12
  • 36
vangorra
  • 1,631
  • 19
  • 24
  • 1
    This is a great solution, used internally in AOSP for many situations. Be careful because it is only appropriate when the the map is small enough to edit without causing alignment problems, and when you will not face localization issues. For example, if you intend to localize only the values, not the keys, this would become a maintenance nightmare if you need to add a new key. But there are plenty of cases where this is the best way to go. – Brent K. Apr 01 '20 at 14:14
  • @BrentK. This is why you should always reference to a string resource, and not a hard coded string – Oz Shabat Jun 24 '20 at 09:40
  • For Kotlin you can use this to create the map from the arrays, `keys.zip(values).toMap()`. – YaMiN Apr 11 '22 at 13:00
33

How can I define a map in XML?

<thisIsMyMap>
  <entry key="foo">bar</entry>
  <entry key="goo">baz</entry>
  <!-- as many more as your heart desires -->
</thisIsMyMap>

Put this in res/xml/, and load it using getResources().getXml(). Walk the events to build up a HashMap<String, String>.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • 1
    This is not what I need. Actually, it is, the table part in XML. But I cannot use getResources() in the static context where I want to build the hash map. – ilomambo Apr 18 '12 at 13:14
  • 2
    @ilomambo: Initialize your hash map in `onCreate()` of a custom `Application` subclass. That is called after any `ContentProviders` you have are created, but before anything else of your app will run. `Application` is a `Context` and should work for parsing this XML. – CommonsWare Apr 18 '12 at 14:00
  • @CommonsWare Thanks for your answer, I will use it. I need to save a HashMap as an XML file, How can I do this? Thanks. – VansFannel Jun 01 '12 at 16:49
  • 3
    This example is incomplete. Writing the XML is easy, writing the parser is slow and a pain. See my example below for a much simpler solution. – vangorra Nov 25 '14 at 15:08
  • @vangorra the answer is correct and a Map like this is much easier to maintain than two arrays if it becomes long. To make it easier to implement this solution I have added a simple parser for this purpose below – Christian Apr 24 '15 at 20:04
  • @CommonsWare can you please tell me how to extract from xml to hashmap – Rahul Feb 18 '16 at 06:24
  • Is it possible to make an array as the value? – android developer Jun 21 '16 at 09:58
  • @androiddeveloper: You can make XML be as complicated as you wish. – CommonsWare Jun 21 '16 at 11:15
  • @CommonsWare How do I put an array of strings inside the value? Currently what I did is put json string inside, which is weird... – android developer Jun 21 '16 at 12:00
  • 1
    @androiddeveloper: `barbazgoo`. Or ``. Or `barbazgoo`. Or whatever. XML can definitely handle collections of elements that you, in Java, elect to model as an array. – CommonsWare Jun 21 '16 at 12:04
  • @CommonsWare All are correct, yes, but what about the parser of Android? In my case, I need to give the xml file as parameter to FirebaseRemoteConfig.setDefaults() method : https://firebase.google.com/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#public-methods . Do you really think it can handle them all the same way? I don't even know if it supports arrays, since its getters don't seem to support getting an array list... :( – android developer Jun 21 '16 at 12:11
  • 1
    @androiddeveloper: "what about the parser of Android?" -- Android has three XML parsers. All can parse that XML. "I need to give the xml file as parameter..." -- Firebase should document what that XML resource needs to look like. Neither you nor I get a vote as to what it should look like. And whether *Firebase* handles arrays is a very separate matter than whether *XML* handles arrays. – CommonsWare Jun 21 '16 at 12:15
  • @CommonsWare Yes, sadly I don't see it in the docs, so I think it's not supported. Thank you and have +1s . – android developer Jun 21 '16 at 12:17
12

You can always embed Json inside your strings.xml file:

res/values/strings.xml

<string name="my_map">{"F":"FOO","B":"BAR"}</string>

And inside your Activity, you can build your Map in the onStart method:

private HashMap<String, String> myMap;

@Override
protected void onStart() {
    super.onStart();
    myMap = new Gson().fromJson(getString(R.string.my_map), new TypeToken<HashMap<String, String>>(){}.getType());
}

This code needs Google Gson API to work. You can do it using the built-in Json API in the Android SDK.

And As for accessing the Map statically, you can create a static method:

private static HashMap<String, String> method(Context context) {
    HashMap<String, String> myMap = new Gson().fromJson(context.getString(R.string.serve_times), new TypeToken<HashMap<String, String>>(){}.getType());
    return myMap;
}
Shahood ul Hassan
  • 745
  • 2
  • 9
  • 19
Ayesh Qumhieh
  • 1,117
  • 2
  • 11
  • 26
6

The correct answer was mentioned by CommonsWare above, but as XML-parsing is not so simple, as following a simple parser for this purpose:

public static Map<String, String> getHashMapResource(Context context, int hashMapResId) {
    Map<String, String> map = new HashMap<>();
    XmlResourceParser parser = context.getResources().getXml(hashMapResId);

    String key = null, value = null;

    try {
        int eventType = parser.getEventType();

        while (eventType != XmlPullParser.END_DOCUMENT) {
            if (eventType == XmlPullParser.START_TAG) {
                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;
}
Christian
  • 4,596
  • 1
  • 26
  • 33
  • 1
    as I wrote the correct answer was mentioned by Commonsware, it was only an addition. I thought it is not usefull to copy the answer of Commonsware again in my answer – Christian Feb 09 '17 at 22:17
0

Android often works with DefaultsXmlParser.getDefaultsFromXml() which parse next syntax:

<?xml version="1.0" encoding="utf-8"?>
<defaults>
    <entry>
        <key>api_url</key>
        <value>https://</value>
    </entry>
    <entry>
        <key>some_feature_flag</key>
        <value>true</value>
    </entry>
</defaults>

And read map:

val map = DefaultsXmlParser.getDefaultsFromXml(this, R.xml.my_map)
Pavel Shorokhov
  • 4,485
  • 1
  • 35
  • 44