- The mysterious case of the Bundle and the Map
- When edge cases just aren’t covered
- On Parcels
- Down into the rabbit hole
- Step one: finding the first weak link
- Step two: writing the map
- Step three: writing maps’ values
- Step four: writing a Map to the Parcel
- Step five: reading back the Map
- Workaround (aka tl;dr)
- Another possible workaround
- When you don’t control upstream Intents
- Conclusion
- Bundle in Android with Example
- Using the Bundle in the Android App
- Как добавить карту
- Обзор
- Ознакомьтесь с кодом
- Как добавить карту
- Добавьте объект SupportMapFragment
- Как добавить фрагмент статически
- Как добавить фрагмент динамически
The mysterious case of the Bundle and the Map
Because putting Maps in a Bundle is harder than it looks.
[This post was written with the help of Eugenio Marletti ]
⚠️ Warning — this is a long post.
When edge cases just aren’t covered
Assume that you need to pass a map of values as an extra in an Intent. This might not be a common scenario, admittedly, but it can happen. It definitely happened to Eugenio.
If you are using a HashMap , the most common type of Map , and you didn’t create a custom class that contains “extra” information, you’re lucky. You can put in:
And in your receiving Activity , you’ll get your nice map back out of the Intent ’s extras:
But what if you need to pass another kind of Map in the intent extras — say, a TreeMap (or any custom implementation)? Well, when you retrieve it:
Then you get this:
Yep, a nice ClassCastException , because your map has turned into… a HashMap .
We’ll see why we’re using getSerializableExtra() later on, suffice for now saying it’s because all the default Map implementations are Serializable and there’s no narrower scope of putExtra() / get*Extra() that can accept them.
Before we move on, let’s get to know the actors involved in this drama.
[tl:dr; skip to “Workaround” at the end if you just want a solution!]
On Parcels
Many of you will know (but maybe some don’t) that all the IPC communication in the Android framework is based upon the concept of Binder s. And hopefully many of you know that the main mechanism to allow data marshalling between those processes is based on Parcel s.
A Parcel is an optimised, non-general purpose serialisation mechanism that Android employs for IPC. Contrary to Serializable objects, you should never use Parcel s for any kind of persistence, as it does not provision for handling different versions of the data. Whenever you see a Bundle , you’re dealing with a Parcel under the hood.
Setting arguments on a Fragment? Parcel.
Parcel s know how to handle a bunch of types out of the box, including native types, strings, arrays, maps, sparse arrays, parcelables and serializables. Parcelable s are the mechanism that you (should) use to write and read arbitrary data to a Parcel , unless you really, really need to use Serializable .
The advantages of Parcelable over Serializable are mostly about performances, and that should be enough of a reason to prefer the former in most cases, as Serializable comes with a certain overhead.
Down into the rabbit hole
So, let’s try to understand what makes us get a ClassCastException . Starting from the code we are using, we can see that our call to Intent#putExtras() resolves to the overload that takes a String and a Serializable . As we’ve said before, this is expected, as Map implementations are Serializable , and they aren’t Parcelable . There also isn’t a putExtras() that explicitly takes a Map .
Step one: finding the first weak link
Let’s look at what happens in Intent.putExtra(String, Serializable) :
In here, mExtras is clearly a Bundle . So ok, Intent delegates all the extras to a bundle, just as we expected, and calls Bundle#putSerializable() . Let’s see what that method does:
As it turns out, this just in turn delegates to the super implementation, which is:
Good, we got to some meat, at last.
First of all, let’s ignore unparcel() . We can notice that mMap is an ArrayMap . This tells us we’re losing any kind of type information we might have had before — i.e., at this point, everything ends up in one big map that contains Object s as values, no matter how strongly typed the method we used to put the value in the Bundle was.
Our spider sense starts to tingle…
Step two: writing the map
The really interesting stuff starts to happen when we get to actually writing the contents of the Bundle to a Parcel . Until then, if we check the type of our extra, we’re still getting the correct type:
That prints, as we’d expect, TreeMap to the LogCat. So the transformation must happen between the time the Bundle gets written into the Parcel , and when it’s read again.
If we look at how writing to a Parcel happens, we see that the nitty gritty goes down in BaseBundle#writeToParcelInner :
Skipping all the code that is irrelevant for us, we see that the bulk of the work is performed by Parcel#writeArrayMapInternal() (remember that mMap is an ArrayMap ):
What this basically does is it writes every key-value pair in the BaseBundle ’s mMap sequentially as a String (the keys are all strings here) followed by the value. The latter seems not to be considering the value type so far.
Step three: writing maps’ values
So how does Parcel#writeValue() look like, you ask? Here it is, in its if-elseif-else glory:
Aha! Gotcha! Even though we put our TreeMap in the bundle as a Serializable , the writeValue() method does in fact catch it in its v instanceOf Map branch, which (for obvious reasons) comes before the else … if (v instanceOf Serializable) branch.
At this point, the smell is getting really strong.
I now wonder, are they using some totally undocumented shortcut for Map s, that somehow turns them into HashMap s?
Step four: writing a Map to the Parcel
Well, as it turns out, writeMap() doesn’t do an awful lot in and by itself, apart from enforcing the type of Map we’ll be handling later on:
The JavaDoc for this method is pretty clear:
“The Map keys must be String objects.”
Type erasure makes sure we’ll actually never have a runtime error here, though, even if we might be passing a Map with keys that aren’t Strings (again, this is totally undocumented at higher level…).
In fact, as soon as we take a look at writeMapInternal() , this hits us:
Again, type erasure here makes all those casts pretty much worthless at runtime. The fact is that we’re relying on our old type-checking friend writeValue() for both the keys and the values as we “unpack” the map and just dump everything in the Parcel . And as we’ve seen, writeValue() is perfectly able to handle non- String keys.
Maybe the documentation got out of sync with the code at some point here, but as a matter of fact, putting and retrieving a TreeMap in a Bundle works perfectly.
Well, with the exception of the TreeMap becoming an HashMap , of course.
Ok, the picture here is pretty clear by now. Map s completely lose their type when they’re written to a Parcel , so there’s no way to recover that information when they get read back.
Step five: reading back the Map
As a last quick check of our theory, let’s go and check readValue() , which is writeValue() ’s counterpart:
The way Parcel works when writing data is, for each item it contains:
- it writes an int that defines the data type (one of the VAL_* constants)
- dumps the data itself (optionally including other metadata such as the data length for non-fixed size types, e.g. String ).
- recursively repeat for nested (non-primitive) data types
Here we see that readValue() reads that data type int , that for our TreeMap was set to VAL_MAP by writeValue() , and then the corresponding switch case simply calls readHashMap() to retrieve the data itself:
(the C#-style opening curly brace is actually in AOSP, it’s not my fault)
You can pretty much imagine that readMapInternal() simply repacks all map items it reads from the Parcel into the map that we pass to it.
And yes. This is the reason why you get always a HashMap back from a Bundle . The same goes if you create a custom Map that implements Parcelable . Definitely not what we’d expect!
It’s hard to say if this is an intended effect or simply an oversight. It’s admittedly an edge case, since you have really few valid reasons to pass a Map into an Intent , and you should have just as little good reasons to pass Serializable s instead of Parcelable s. But the lack of documentation makes me think it might actually be an oversight rather than a design decision.
Workaround (aka tl;dr)
Ok, we’ve understood our issue in depth, and now we’ve identified the critical path that messes with us. We need to make sure our TreeMap doesn’t get caught into the v instanceOf Map check in writeValue() .
The first solution that came to my mind when talking to Eugenio was ugly but effective: wrap the map into a Serializable container. Eugenio quickly whipped up this generic wrapper and confirmed it solves the issue.
Please note the gist is using the Android’s @NonNull annotation to enforce its contracts. If you want to use this in pure Java modules, you can replace it with JetBrains’ @NotNull , or you could strip those annotations altogether.
Another possible workaround
Another solution could be to pre-serialize the Map yourself into a byte array before putting it as an Intent extra, and then retrieving it with getByteArrayExtra() , but you’d then have to handle serialisation and deserialisation manually.
In case you masochistically wanted to opt for this other solution instead, Eugenio has provided a separate Gist with the code.
When you don’t control upstream Intents
Lastly, maybe for some reason you can’t control the Bundle creation code — e.g., because it’s in some third-party library.
In that case, remember that many Map implementations have a constructor that takes a Map as input, like new TreeMap(Map) . You can use that constructor, if needed, to “change back” the HashMap you retrieve from the Bundle into your preferred Map type.
Keep in mind that in this case any “extra” properties on that map will be lost and only the key/value pairs will be preserved.
Conclusion
Being an Android developer means juggling your way around pretty much everything, especially the small, seemingly insignificant things.
What can we learn from this?
When things don’t work as you’d expect them,
don’t just stare at the JavaDoc.
Because that might be outdated.
Or because the authors of the JavaDoc
didn’t know about your specific case.
The answer might be in the AOSP code.
We have the huge luxury (and curse) of having access to the AOSP code. That’s something almost unique in the mobile landscape. We can know to a certain extent exactly what goes on. And we should.
Because even though it might look like it’s WTF-land sometimes, you can only become a better developer when you get to know the inner workings of the platform you work on.
And remember: what doesn’t kill you makes you stronger. Or crazier.
Источник
Bundle in Android with Example
It is known that Intents are used in Android to pass to the data from one activity to another. But there is one another way, that can be used to pass the data from one activity to another in a better way and less code space ie by using Bundles in Android. Android Bundles are generally used for passing data from one activity to another. Basically here concept of key-value pair is used where the data that one wants to pass is the value of the map, which can be later retrieved by using the key. Bundles are used with intent and values are sent and retrieved in the same fashion, as it is done in the case of Intent. It depends on the user what type of values the user wants to pass, but bundles can hold all types of values (int, String, boolean, char) and pass them to the new activity.
The following are the major types that are passed/retrieved to/from a Bundle:
putInt(String key, int value), getInt(String key, int value)
putString(String key, String value), getString(String key, String value)
putStringArray(String key, String[] value), getStringArray(String key, String[] value)
putChar(String key, char value), getChar(String key, char value)
putBoolean(String key, boolean value), getBoolean(String key, boolean value)
Using the Bundle in the Android App
The bundle is always used with Intent in Android. Now to use Bundle writes the below code in the MainActivity.
Источник
Как добавить карту
В этой статье рассказывается, как добавить базовую карту в приложение Android, когда вы уже настроили проект для использования Maps SDK для Android. После добавления карты вы можете изменить ее тип и функции.
Обзор
Maps SDK для Android поддерживает несколько классов, с помощью можно управлять жизненным циклом, функциями и данными карты в приложении. Эти классы поддерживают взаимодействие с пользователями на основе модели интерфейса Android. Например, вы можете задать исходное состояние карты и ее реакцию на жесты пользователя во время выполнения.
Основной интерфейс и классы для работы с картами:
GoogleMap – точка входа для управления перечисленными ниже функциями и данными. Приложение может получить доступ к объекту GoogleMap после того, как он был извлечен из объекта SupportMapFragment или MapView .
SupportMapFragment – фрагмент для управления жизненным циклом объекта GoogleMap .
MapView – представление для управления жизненным циклом объекта GoogleMap .
OnMapReadyCallback – интерфейс обратного вызова, который обрабатывает события и взаимодействия с пользователями для объекта GoogleMap .
Объект GoogleMap автоматически выполняет следующие операции:
- подключение к Google Картам;
- загрузка фрагментов карты;
- отображение фрагментов на экране устройства;
- отображение элементов управления (например, панорамирования и масштабирования);
- изменение вида карты в ответ на жесты панорамирования и масштабирования.
Чтобы использовать в приложении объект GoogleMap , вам необходимо добавить объект SupportMapFragment или MapView в качестве контейнера для карты, а затем извлечь из этого контейнера объект GoogleMap . Поскольку классы-контейнеры являются производными от фрагмента или представления Android, они обеспечивают возможности управления жизненным циклом карты и работы с интерфейсом, доступные в своих базовых классах Android. Класс SupportMapFragment – более современный и часто используемый контейнер для объекта GoogleMap .
Ознакомьтесь с кодом
Приведенный ниже код взят из полного объекта activity (Java), которая используется в этом разделе при статическом добавлении фрагмента. Проект Android был создан на основе шаблона пустого проекта, а затем обновлен по инструкциям из этого руководства. После выполнения действий, описанных в этой статье, ваш код может отличаться от приведенного ниже. Это зависит от шаблона проекта.
Как добавить карту
В этом разделе рассказывается, как добавить базовую карту, используя фрагмент как контейнер. Вы также можете выбрать вариант с представлением. Пример: RawMapViewDemoActivity на GitHub.
Подготовка. Убедитесь, что вы выполнили действия, описанные в руководстве по конфигурации проекта, чтобы обеспечить поддержку Maps SDK для Android.
Добавьте объект SupportMapFragment в объект activity, который отвечает за обработку карты. Фрагмент можно добавить статически или динамически.
Реализуйте интерфейс OnMapReadyCallback .
Задайте файл шаблона в качестве представления контента.
Если вы добавили фрагмент статически, получите дескриптор фрагмента.
Зарегистрируйте обратный вызов.
Получите дескриптор объекта GoogleMap .
Добавьте объект SupportMapFragment
Объект SupportMapFragment можно добавить в приложение статически или динамически. Статически это сделать проще. Если же вы добавите фрагмент динамически, то сможете выполнять с ним дополнительные действия, например удалять или заменять код во время выполнения.
Как добавить фрагмент статически
В файле макета для объекта activity, который обеспечивает работу карты, выполните следующие действия:
- Добавьте элемент fragment .
- Добавьте объявление имени xmlns:map=»http://schemas.android.com/apk/res-auto» . Это позволит использовать изменяемые атрибуты XML для maps .
- В элементе fragment задайте для атрибута android:name значение com.google.android.gms.maps.SupportMapFragment .
- В элементе fragment добавьте атрибут android:id и задайте для него значение, соответствующее идентификатору ресурса R.id.map ( @+id/map ).
Полный пример кода для файла макета, где есть элемент fragment :
Как добавить фрагмент динамически
Выполните следующие действия для объекта activity:
- Создайте экземпляр SupportMapFragment .
- Выполните транзакцию, чтобы добавить фрагмент в объект activity. Подробнее…
Источник