public class

BundleMarshaller

extends java.lang.Object

 java.lang.Object

↳androidx.car.navigation.utils.BundleMarshaller

Gradle dependencies

compile group: 'androidx.car', name: 'car', version: '1.0.0-alpha7'

  • groupId: androidx.car
  • artifactId: car
  • version: 1.0.0-alpha7

Artifact androidx.car:car:1.0.0-alpha7 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.car:car com.android.support:car

Overview

Class responsible for serializing and deserializing data into a . It also provides a way to detect what items in the have been modified during marshalling.

A single BundleMarshaller can be re-used to serialize or deserialize data multiple times. Similarity, deserialization can be done in-place, updating existing Bundlables. This reduces the number of instances being allocated.

When serializing, use BundleMarshaller.resetBundle() before marshalling and BundleMarshaller.getBundle() to obtain an snap-shot of the serialized content. Or use BundleMarshaller.resetDelta() and BundleMarshaller.getDelta() to obtain a representing the patch between the last and the new serialized data.

When deserializing, use BundleMarshaller.setBundle(Bundle) to deserialize a containing an snap-shot, or BundleMarshaller.applyDelta(Bundle) to process a patch from the last deserialized data.

Keys used in the "get" and "put" methods must be lower camel case alphanumerical identifiers (e.g.: "distanceUnit"). Symbols like "." and "_" are reserved by the system.

When deserializing java.util.List objects, this class assumes that they implement random access (e.g. java.util.ArrayList), or they are relatively small (see more details at BundleMarshaller)

Summary

Constructors
publicBundleMarshaller()

Methods
public voidapplyDelta(Bundle delta)

Merges the provided on top of the one stored in this BundleMarshaller.

public booleangetBoolean(java.lang.String key)

Returns the value associated with the given key, or false if no mapping of the desired type exists for the given key.

public BundlablegetBundlable(java.lang.String key, Bundlable current, java.util.function.Supplier<Bundlable> factory)

Returns the value associated with the given key, or null if no mapping of the desired type exists for the given key.

public java.util.List<Bundlable>getBundlableList(java.lang.String key, java.util.List<Bundlable> current, java.util.function.Supplier<Bundlable> factory)

Returns the value associated with the given key, or null if no mapping of the desired type exists for the given key.

public java.util.List<Bundlable>getBundlableListNonNull(java.lang.String key, java.util.List<Bundlable> current, java.util.function.Supplier<Bundlable> factory)

Returns the value associated with the given key, or an empty list if no mapping of the desired type exists for the given key.

public BundlablegetBundlableNonNull(java.lang.String key, Bundlable current, java.util.function.Supplier<Bundlable> factory)

Returns the value associated with the given key, or a default value if no mapping of the desired type exists for the given key.

public BundlegetBundle()

Returns data serialized since the last time this instance was constructed, or BundleMarshaller.resetBundle() was called.

public BundlegetDelta()

Gets a containing only the entries of BundleMarshaller.getBundle() that were modified since this instance was constructed, or BundleMarshaller.resetDelta() was called.

public doublegetDouble(java.lang.String key)

Returns the value associated with the given key, or 0.0 if no mapping of the desired type exists for the given key.

public java.lang.Enum<E>getEnum(java.lang.String key, java.lang.Class<java.lang.Enum> clazz)

Returns the value associated with the given key, or null if no mapping of the desired type exists for the given key.

public java.lang.Enum<E>getEnumNonNull(java.lang.String key, java.lang.Class<java.lang.Enum> clazz, java.lang.Enum<E> defaultValue)

Returns the value associated with the given key, or the provided default value if no mapping of the desired type exists for the given key.

public floatgetFloat(java.lang.String key)

Returns the value associated with the given key, or 0.0f if no mapping of the desired type exists for the given key.

public intgetInt(java.lang.String key)

Returns the value associated with the given key, or 0 if no mapping of the desired type exists for the given key.

public java.lang.StringgetString(java.lang.String key)

Returns the value associated with the given key, or null if no mapping of the desired type exists for the given key.

public java.lang.StringgetStringNonNull(java.lang.String key, java.lang.String defaultValue)

Returns the value associated with the given key, or the provided default value if no mapping of the desired type exists for the given key.

public voidputBoolean(java.lang.String key, boolean value)

Inserts a boolean value, replacing any existing value for the given key.

public voidputBundlable(java.lang.String key, Bundlable value)

Inserts a Bundlable value, replacing any existing value for the given key.

public voidputBundlableList(java.lang.String key, java.util.List<Bundlable> values)

Inserts a java.util.List of Bundlable values, replacing any existing value for the given key.

public voidputDouble(java.lang.String key, double value)

Inserts a double value, replacing any existing value for the given key.

public voidputEnum(java.lang.String key, java.lang.Enum<E> value)

Inserts an enum value, replacing any existing value for the given key.

public voidputFloat(java.lang.String key, float value)

Inserts a float value, replacing any existing value for the given key.

public voidputInt(java.lang.String key, int value)

Inserts an int value, replacing any existing value for the given key.

public voidputString(java.lang.String key, java.lang.String value)

Inserts a string value, replacing any existing value for the given key.

public voidresetBundle()

Resets this BundleMarshaller causing BundleMarshaller.getBundle() to return an empty until the next marshalling is executed.

public voidresetDelta()

Resets tracking of modified entries, causing BundleMarshaller.getDelta() to return an empty until the next marshalling is executed.

public voidsetBundle(Bundle bundle)

Replaces the to serialize into or deserialize from.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public BundleMarshaller()

Methods

public Bundle getBundle()

Returns data serialized since the last time this instance was constructed, or BundleMarshaller.resetBundle() was called.

public void resetBundle()

Resets this BundleMarshaller causing BundleMarshaller.getBundle() to return an empty until the next marshalling is executed. This can be used occasionally to remove unused keys in the .

public void setBundle(Bundle bundle)

Replaces the to serialize into or deserialize from.

public Bundle getDelta()

Gets a containing only the entries of BundleMarshaller.getBundle() that were modified since this instance was constructed, or BundleMarshaller.resetDelta() was called.

public void applyDelta(Bundle delta)

Merges the provided on top of the one stored in this BundleMarshaller.

Parameters:

delta: a containing entries to be updated on one stored in this BundleMarshaller instance. Such can be produced by using the BundleMarshaller.resetDelta() and BundleMarshaller.getDelta() methods during data serialization.

public void resetDelta()

Resets tracking of modified entries, causing BundleMarshaller.getDelta() to return an empty until the next marshalling is executed. This can be used between serializations make BundleMarshaller.getDelta() return only the differences.

public void putInt(java.lang.String key, int value)

Inserts an int value, replacing any existing value for the given key.

Parameters:

key: lower camel case alphanumerical identifier
value: an int

public int getInt(java.lang.String key)

Returns the value associated with the given key, or 0 if no mapping of the desired type exists for the given key.

Parameters:

key: lower camel case alphanumerical identifier

Returns:

an int

public void putFloat(java.lang.String key, float value)

Inserts a float value, replacing any existing value for the given key.

Parameters:

key: lower camel case alphanumerical identifier
value: a float

public float getFloat(java.lang.String key)

Returns the value associated with the given key, or 0.0f if no mapping of the desired type exists for the given key.

Parameters:

key: lower camel case alphanumerical identifier

Returns:

a float

public void putDouble(java.lang.String key, double value)

Inserts a double value, replacing any existing value for the given key.

Parameters:

key: lower camel case alphanumerical identifier
value: a double

public double getDouble(java.lang.String key)

Returns the value associated with the given key, or 0.0 if no mapping of the desired type exists for the given key.

Parameters:

key: lower camel case alphanumerical identifier

Returns:

a double

public void putBoolean(java.lang.String key, boolean value)

Inserts a boolean value, replacing any existing value for the given key.

Parameters:

key: lower camel case alphanumerical identifier
value: a boolean

public boolean getBoolean(java.lang.String key)

Returns the value associated with the given key, or false if no mapping of the desired type exists for the given key.

Parameters:

key: lower camel case alphanumerical identifier

Returns:

a boolean

public void putString(java.lang.String key, java.lang.String value)

Inserts a string value, replacing any existing value for the given key.

Parameters:

key: lower camel case alphanumerical identifier
value: a string, or null

public java.lang.String getString(java.lang.String key)

Returns the value associated with the given key, or null if no mapping of the desired type exists for the given key.

Parameters:

key: lower camel case alphanumerical identifier

Returns:

a string, or null

public java.lang.String getStringNonNull(java.lang.String key, java.lang.String defaultValue)

Returns the value associated with the given key, or the provided default value if no mapping of the desired type exists for the given key.

Parameters:

key: lower camel case alphanumerical identifier
defaultValue: value to return if key does not exist or if a null value is associated with the given key.

Returns:

a string

public void putEnum(java.lang.String key, java.lang.Enum<E> value)

Inserts an enum value, replacing any existing value for the given key. The provided enum will be serialized as a string using name.

Parameters:

key: lower camel case alphanumerical identifier
value: an enum, or null

public java.lang.Enum<E> getEnum(java.lang.String key, java.lang.Class<java.lang.Enum> clazz)

Returns the value associated with the given key, or null if no mapping of the desired type exists for the given key.

Parameters:

key: lower camel case alphanumerical identifier
clazz: java.lang.Enum class to be used to deserialize the value.

Returns:

an enum, or null

public java.lang.Enum<E> getEnumNonNull(java.lang.String key, java.lang.Class<java.lang.Enum> clazz, java.lang.Enum<E> defaultValue)

Returns the value associated with the given key, or the provided default value if no mapping of the desired type exists for the given key.

Parameters:

key: lower camel case alphanumerical identifier
clazz: java.lang.Enum class to be used to deserialize the value.
defaultValue: value to return if key does not exist or if a null value is associated with the given key.

Returns:

an enum

public void putBundlable(java.lang.String key, Bundlable value)

Inserts a Bundlable value, replacing any existing value for the given key.

Parameters:

key: lower camel case alphanumerical identifier
value: a Bundlable, or null

public Bundlable getBundlable(java.lang.String key, Bundlable current, java.util.function.Supplier<Bundlable> factory)

Returns the value associated with the given key, or null if no mapping of the desired type exists for the given key. If a non-null "current" instance is provided, then the deserialization would be done in place. Otherwise, a new instance will be created using the provided factory.

Parameters:

key: lower camel case alphanumerical identifier
current: current value (if available) to perform in-place deserialization, or null
factory: a java.util.function.Supplier capable of providing an instance of a Bundlable of type T. The suggested implementation is to pass a reference to the default constructor of that class.

Returns:

an instance of type T, or null

public Bundlable getBundlableNonNull(java.lang.String key, Bundlable current, java.util.function.Supplier<Bundlable> factory)

Returns the value associated with the given key, or a default value if no mapping of the desired type exists for the given key. If a non-null value is available, then such value will be deserialized in-place on the given "current" instance. Otherwise, a default value will be generated using the provided factory.

Parameters:

key: lower camel case alphanumerical identifier
current: current value to perform in-place deserialization
factory: a java.util.function.Supplier capable of providing an instance of a Bundlable of type T. The suggested implementation is to pass a reference to the default constructor of that class.

Returns:

an instance of type T

public void putBundlableList(java.lang.String key, java.util.List<Bundlable> values)

Inserts a java.util.List of Bundlable values, replacing any existing value for the given key.

Parameters:

key: lower camel case alphanumerical identifier
values: a java.util.List of Bundlable values, or null

public java.util.List<Bundlable> getBundlableList(java.lang.String key, java.util.List<Bundlable> current, java.util.function.Supplier<Bundlable> factory)

Returns the value associated with the given key, or null if no mapping of the desired type exists for the given key. If a non-null "current" list is provided, then the deserialization would be done in place. Otherwise, a new list will be created and items will be instantiated using the provided factory.

Parameters:

key: lower camel case alphanumerical identifier
current: current value (if available) to perform in-place deserialization, or null
factory: a java.util.function.Supplier capable of providing an instance of a Bundlable of type T. The suggested implementation is to pass a reference to the default constructor of that class.

Returns:

a list of instances of type T, or null. The resulting list might contain null elements.

public java.util.List<Bundlable> getBundlableListNonNull(java.lang.String key, java.util.List<Bundlable> current, java.util.function.Supplier<Bundlable> factory)

Returns the value associated with the given key, or an empty list if no mapping of the desired type exists for the given key. If a non-null "current" list is provided, then the deserialization would be done in place. Otherwise, a new list will be created and items will be instantiated using the provided factory.

Parameters:

key: lower camel case alphanumerical identifier
current: current value (if available) to perform in-place deserialization, or null
factory: a java.util.function.Supplier capable of providing an instance of a Bundlable of type T. The suggested implementation is to pass a reference to the default constructor of that class.

Returns:

a list of instances of type T, or an empty list. The resulting list might contain null elements.

Source

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.car.navigation.utils;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;

import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;

/**
 * Class responsible for serializing and deserializing data into a {@link Bundle}. It also
 * provides a way to detect what items in the {@link Bundle} have been modified during
 * marshalling.
 * <p>
 * A single {@link BundleMarshaller} can be re-used to serialize or deserialize data multiple times.
 * Similarity, deserialization can be done in-place, updating existing {@link Bundlable}s. This
 * reduces the number of instances being allocated.
 * <p>
 * When serializing, use {@link #resetBundle()} before marshalling and {@link #getBundle()} to
 * obtain an snap-shot of the serialized content. Or use {@link #resetDelta()} and
 * {@link #getDelta()} to obtain a {@link Bundle} representing the patch between the last and
 * the new serialized data.
 * <p>
 * When deserializing, use {@link #setBundle(Bundle)} to deserialize a {@link Bundle} containing an
 * snap-shot, or {@link #applyDelta(Bundle)} to process a patch from the last deserialized data.
 * <p>
 * Keys used in the "get" and "put" methods must be lower camel case alphanumerical identifiers
 * (e.g.: "distanceUnit"). Symbols like "." and "_" are reserved by the system.
 * <p>
 * When deserializing {@link List} objects, this class assumes that they implement random access
 * (e.g. {@link ArrayList}), or they are relatively small (see more details at
 * {@link #trimList(List, int)})
 *
 * @hide
 */
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class BundleMarshaller {
    /**
     * Separator used to concatenate identifiers when marshalling non-primitive types (e.g. lists
     * or {@link Bundlable}s).
     */
    private static final String KEY_SEPARATOR = ".";
    /**
     * Identifier used to record if a given non-primitive field is null or not. This allows
     * serializing null objects without the need of using reflection or static methods.
     */
    private static final String IS_NULL_KEY = "_isNull";
    /**
     * Identifier used to record the length of a collection. This allows serializing changes to the
     * length of a collection without having to remove elements or having to iterate over every
     * possible collection key.
     */
    private static final String SIZE_KEY = "_size";
    /**
     * Special value for {@link #SIZE_KEY} to serialize a null collection.
     */
    private static final int NULL_SIZE = -1;

    private Bundle mBundle = new Bundle();
    private String mKeyPrefix = "";
    private final Bundle mBundleDelta = new Bundle();

    /**
     * Returns data serialized since the last time this instance was constructed, or
     * {@link #resetBundle()} was called.
     */
    public Bundle getBundle() {
        return mBundle;
    }

    /**
     * Resets this {@link BundleMarshaller} causing {@link #getBundle()} to return an empty
     * {@link Bundle} until the next marshalling is executed. This can be used occasionally to
     * remove unused keys in the {@link Bundle}.
     */
    public void resetBundle() {
        mBundle.clear();
    }

    /**
     * Replaces the {@link Bundle} to serialize into or deserialize from.
     */
    public void setBundle(Bundle bundle) {
        mBundle = bundle;
    }

    /**
     * Gets a {@link Bundle} containing only the entries of {@link #getBundle()} that were modified
     * since this instance was constructed, or {@link #resetDelta()} was called.
     */
    public Bundle getDelta() {
        return mBundleDelta;
    }

    /**
     * Merges the provided {@link Bundle} on top of the one stored in this {@link BundleMarshaller}.
     *
     * @param delta a {@link Bundle} containing entries to be updated on one stored in this
     *              {@link BundleMarshaller} instance. Such {@link Bundle} can be produced by
     *              using the {@link #resetDelta()} and {@link #getDelta()} methods during data
     *              serialization.
     */
    public void applyDelta(Bundle delta) {
        mBundle.putAll(delta);
    }

    /**
     * Resets tracking of modified entries, causing {@link #getDelta()} to return an empty
     * {@link Bundle} until the next marshalling is executed. This can be used between
     * serializations make {@link #getDelta()} return only the differences.
     */
    public void resetDelta() {
        mBundleDelta.clear();
    }

    /**
     * Inserts an int value, replacing any existing value for the given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @param value an int
     */
    public void putInt(@NonNull String key, int value) {
        String mangledKey = getMangledKey(key);
        if (!mBundle.containsKey(mangledKey) || mBundle.getInt(mangledKey) != value) {
            mBundleDelta.putInt(mangledKey, value);
            mBundle.putInt(mangledKey, value);
        }
    }

    /**
     * Returns the value associated with the given key, or 0 if no mapping of the desired type
     * exists for the given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @return an int
     */
    public int getInt(@NonNull String key) {
        return mBundle.getInt(getMangledKey(key));
    }

    /**
     * Inserts a float value, replacing any existing value for the given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @param value a float
     */
    public void putFloat(@NonNull String key, float value) {
        String mangledKey = getMangledKey(key);
        if (!mBundle.containsKey(mangledKey)
                || Float.compare(mBundle.getFloat(mangledKey), value) != 0) {
            mBundleDelta.putFloat(mangledKey, value);
            mBundle.putFloat(mangledKey, value);
        }
    }

    /**
     * Returns the value associated with the given key, or 0.0f if no mapping of the desired type
     * exists for the given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @return a float
     */
    public float getFloat(@NonNull String key) {
        return mBundle.getFloat(getMangledKey(key));
    }

    /**
     * Inserts a double value, replacing any existing value for the given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @param value a double
     */
    public void putDouble(@NonNull String key, double value) {
        String mangledKey = getMangledKey(key);
        if (!mBundle.containsKey(mangledKey)
                || Double.compare(mBundle.getDouble(mangledKey), value) != 0) {
            mBundleDelta.putDouble(mangledKey, value);
            mBundle.putDouble(mangledKey, value);
        }
    }

    /**
     * Returns the value associated with the given key, or 0.0 if no mapping of the desired type
     * exists for the given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @return a double
     */
    public double getDouble(@NonNull String key) {
        return mBundle.getDouble(getMangledKey(key));
    }

    /**
     * Inserts a boolean value, replacing any existing value for the given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @param value a boolean
     */
    public void putBoolean(@NonNull String key, boolean value) {
        String mangledKey = getMangledKey(key);
        if (!mBundle.containsKey(mangledKey) || mBundle.getBoolean(mangledKey) != value) {
            mBundleDelta.putBoolean(mangledKey, value);
            mBundle.putBoolean(mangledKey, value);
        }
    }

    /**
     * Returns the value associated with the given key, or false if no mapping of the desired type
     * exists for the given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @return a boolean
     */
    public boolean getBoolean(@NonNull String key) {
        return mBundle.getBoolean(getMangledKey(key));
    }

    /**
     * Inserts a string value, replacing any existing value for the given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @param value a string, or null
     */
    public void putString(@NonNull String key, @Nullable String value) {
        String mangledKey = getMangledKey(key);
        if (!mBundle.containsKey(mangledKey)
                || !Objects.equals(mBundle.getString(mangledKey), value)) {
            mBundleDelta.putString(mangledKey, value);
            mBundle.putString(mangledKey, value);
        }
    }

    /**
     * Returns the value associated with the given key, or null if no mapping of the desired type
     * exists for the given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @return a string, or null
     */
    @Nullable
    public String getString(@NonNull String key) {
        return mBundle.getString(getMangledKey(key));
    }

    /**
     * Returns the value associated with the given key, or the provided default value if no mapping
     * of the desired type exists for the given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @param defaultValue value to return if key does not exist or if a null value is associated
     *                     with the given key.
     * @return a string
     */
    @NonNull
    public String getStringNonNull(@NonNull String key, @NonNull String defaultValue) {
        return mBundle.getString(getMangledKey(key), defaultValue);
    }

    /**
     * Inserts an enum value, replacing any existing value for the given key. The provided enum
     * will be serialized as a string using {@link Enum#name()}.
     *
     * @param key lower camel case alphanumerical identifier
     * @param value an enum, or null
     */
    public <T extends Enum<T>> void putEnum(@NonNull String key, @Nullable T value) {
        putString(key, value != null ? value.name() : null);
    }

    /**
     * Returns the value associated with the given key, or null if no mapping of the desired type
     * exists for the given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @param clazz {@link Enum} class to be used to deserialize the value.
     * @param <T> {@link Enum} type to be returned.
     * @return an enum, or null
     */
    @Nullable
    public <T extends Enum<T>> T getEnum(@NonNull String key, @NonNull Class<T> clazz) {
        String name = getString(key);
        try {
            return name != null ? Enum.valueOf(clazz, name) : null;
        } catch (IllegalArgumentException ex) {
            return null;
        }
    }

    /**
     * Returns the value associated with the given key, or the provided default value if no mapping
     * of the desired type exists for the given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @param clazz {@link Enum} class to be used to deserialize the value.
     * @param defaultValue value to return if key does not exist or if a null value is associated
     *                     with the given key.
     * @param <T> {@link Enum} type to be returned.
     * @return an enum
     */
    @NonNull
    public <T extends Enum<T>> T getEnumNonNull(@NonNull String key, @NonNull Class<T> clazz,
            @NonNull T defaultValue) {
        T result = getEnum(key, clazz);
        return result != null ? result : defaultValue;
    }

    /**
     * Inserts a {@link Bundlable} value, replacing any existing value for the given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @param value a {@link Bundlable}, or null
     */
    public <T extends Bundlable> void putBundlable(@NonNull String key, @Nullable T value) {
        withKeyPrefix(key, () -> {
            putBoolean(IS_NULL_KEY, value == null);
            if (value != null) {
                value.toBundle(this);
            }
        });
    }

    /**
     * Returns the value associated with the given key, or null if no mapping of the desired type
     * exists for the given key. If a non-null "current" instance is provided, then the
     * deserialization would be done in place. Otherwise, a new instance will be created using the
     * provided factory.
     *
     * @param key lower camel case alphanumerical identifier
     * @param current current value (if available) to perform in-place deserialization, or null
     * @param factory a {@link Supplier} capable of providing an instance of a {@link Bundlable} of
     *                type T. The suggested implementation is to pass a reference to the default
     *                constructor of that class.
     * @param <T> {@link Bundlable} type to be returned.
     * @return an instance of type T, or null
     */
    @Nullable
    public <T extends Bundlable> T getBundlable(@NonNull String key, @Nullable T current,
            @NonNull Supplier<T> factory) {
        return withKeyPrefix(key, () -> {
            if (getBoolean(IS_NULL_KEY)) {
                return null;
            }
            T result = current != null ? current : factory.get();
            result.fromBundle(this);
            return result;
        });
    }

    /**
     * Returns the value associated with the given key, or a default value if no mapping of the
     * desired type exists for the given key. If a non-null value is available, then such value
     * will be deserialized in-place on the given "current" instance. Otherwise, a default value
     * will be generated using the provided factory.
     *
     * @param key lower camel case alphanumerical identifier
     * @param current current value to perform in-place deserialization
     * @param factory a {@link Supplier} capable of providing an instance of a {@link Bundlable} of
     *                type T. The suggested implementation is to pass a reference to the default
     *                constructor of that class.
     * @param <T> {@link Bundlable} type to be returned.
     * @return an instance of type T
     */
    @NonNull
    public <T extends Bundlable> T getBundlableNonNull(@NonNull String key, @NonNull T current,
            @NonNull Supplier<T> factory) {
        T result = getBundlable(key, current, factory);
        return result != null ? result : factory.get();
    }

    /**
     * Inserts a {@link List} of {@link Bundlable} values, replacing any existing value for the
     * given key.
     *
     * @param key lower camel case alphanumerical identifier
     * @param values a {@link List} of {@link Bundlable} values, or null
     */
    public <T extends Bundlable> void putBundlableList(@NonNull String key,
            @Nullable List<T> values) {
        withKeyPrefix(key, () -> {
            putInt(SIZE_KEY, values != null ? values.size() : NULL_SIZE);
            if (values != null) {
                int pos = 0;
                // Using for-each as the provided list might not implement random access (e.g. it
                // might be a linked list).
                for (T value : values) {
                    putBundlable(String.valueOf(pos), value);
                    pos++;
                }
            }
        });
    }

    /**
     * Returns the value associated with the given key, or null if no mapping of the desired type
     * exists for the given key. If a non-null "current" list is provided, then the deserialization
     * would be done in place. Otherwise, a new list will be created and items will be instantiated
     * using the provided factory.
     *
     * @param key lower camel case alphanumerical identifier
     * @param current current value (if available) to perform in-place deserialization, or null
     * @param factory a {@link Supplier} capable of providing an instance of a {@link Bundlable} of
     *                type T. The suggested implementation is to pass a reference to the default
     *                constructor of that class.
     * @param <T> {@link Bundlable} type to be returned.
     * @return a list of instances of type T, or null. The resulting list might contain null
     *         elements.
     */
    @Nullable
    public <T extends Bundlable> List<T> getBundlableList(@NonNull String key,
            @Nullable List<T> current, @NonNull Supplier<T> factory) {
        return withKeyPrefix(key, () -> {
            int listSize = getInt(SIZE_KEY);
            if (listSize == NULL_SIZE) {
                return null;
            }
            List<T> result = current != null ? current : new ArrayList<>(listSize);
            if (result.size() > listSize) {
                result.subList(listSize, result.size()).clear();
            }
            for (int pos = 0; pos < listSize; pos++) {
                String subKey = String.valueOf(pos);
                if (pos < result.size()) {
                    result.set(pos, getBundlable(subKey, result.get(pos), factory));
                } else {
                    result.add(getBundlable(String.valueOf(pos),
                            null /* force the creation of a new instance */,
                            factory));
                }
            }
            return result;
        });
    }

    /**
     * Returns the value associated with the given key, or an empty list if no mapping of the
     * desired type exists for the given key. If a non-null "current" list is provided, then the
     * deserialization would be done in place. Otherwise, a new list will be created and items will
     * be instantiated using the provided factory.
     *
     * @param key lower camel case alphanumerical identifier
     * @param current current value (if available) to perform in-place deserialization, or null
     * @param factory a {@link Supplier} capable of providing an instance of a {@link Bundlable} of
     *                type T. The suggested implementation is to pass a reference to the default
     *                constructor of that class.
     * @param <T> {@link Bundlable} type to be returned.
     * @return a list of instances of type T, or an empty list. The resulting list might contain
     *         null elements.
     */
    @NonNull
    public <T extends Bundlable> List<T> getBundlableListNonNull(@NonNull String key,
            @NonNull List<T> current, @NonNull Supplier<T> factory) {
        List<T> result = getBundlableList(key, current, factory);
        return result != null ? result : new ArrayList<>();
    }

    /**
     * Executes the given {@link Runnable} in a context where {@link #getMangledKey(String)}
     * includes the given key as part of the prefix. Calls to this method can be nested (the
     * provided {@link Runnable} can call to this method if needed). This method should be used when
     * serializing or deserializing nested objects.
     * <p>
     * For example: calling to {@link #withKeyPrefix(String, Runnable)} with "foo" as key and
     * a {@link Runnable} that calls {@link #getMangledKey(String)} with "bar" as key, will
     * cause such {@link #getMangledKey(String)} call to return "foo.bar".
     */
    private void withKeyPrefix(@NonNull String key, @NonNull Runnable runnable) {
        String originalKeyPrefix = mKeyPrefix;
        mKeyPrefix = mKeyPrefix + key + KEY_SEPARATOR;
        runnable.run();
        mKeyPrefix = originalKeyPrefix;
    }

    /**
     * Similar to {@link #withKeyPrefix(String, Runnable)} but allows returning a value.
     */
    private <X> X withKeyPrefix(@NonNull String key, @NonNull Supplier<X> supplier) {
        String originalKeyPrefix = mKeyPrefix;
        mKeyPrefix = mKeyPrefix + key + KEY_SEPARATOR;
        X res = supplier.get();
        mKeyPrefix = originalKeyPrefix;
        return res;
    }

    /**
     * Returns a composed key based on the given one and the current serialization/deserialization
     * key prefix (initially empty). This prefix can be temporarily changed with
     * {@link #withKeyPrefix(String, Runnable)} or {@link #withKeyPrefix(String, Supplier)}.
     */
    private String getMangledKey(@NonNull String key) {
        return mKeyPrefix + key;
    }
}