Skip to content

Instantly share code, notes, and snippets.

@Starksoft
Created May 1, 2026 19:56
Show Gist options
  • Select an option

  • Save Starksoft/241775f1a49324f2ae0a387e1696d4b5 to your computer and use it in GitHub Desktop.

Select an option

Save Starksoft/241775f1a49324f2ae0a387e1696d4b5 to your computer and use it in GitHub Desktop.
Реализация замены bundleOf в Android
package ru.starksoft.core.utils.ext
private const val TAG = "BundleExt"
/**
* Type-safe replacement for the deprecated [androidx.core.os.bundleOf].
*
* Unlike the AndroidX version which accepts `Pair<String, Any?>` and may crash at runtime
* on unsupported types (e.g. `SparseArray`), these overloads constrain the value type at
* compile time. Use one overload per call — all pairs in a single invocation must share
* the same value type.
*
* For mixed-type bundles use [buildBundle].
*
* Example:
* ```
* val bundle = bundleOf("user_id" to userId, "email" to email) // String overload
* ```
*
* See: MOBILE-706.
*/
fun bundleOf(vararg pairs: Pair<String, String?>): Bundle = Bundle(pairs.size).also { bundle ->
for ((key, value) in pairs) bundle.putString(key, value)
}
/** @see bundleOf */
@JvmName("bundleOfInt")
fun bundleOf(vararg pairs: Pair<String, Int>): Bundle = Bundle(pairs.size).also { bundle ->
for ((key, value) in pairs) bundle.putInt(key, value)
}
/** @see bundleOf */
@JvmName("bundleOfLong")
fun bundleOf(vararg pairs: Pair<String, Long>): Bundle = Bundle(pairs.size).also { bundle ->
for ((key, value) in pairs) bundle.putLong(key, value)
}
/** @see bundleOf */
@JvmName("bundleOfBoolean")
fun bundleOf(vararg pairs: Pair<String, Boolean>): Bundle = Bundle(pairs.size).also { bundle ->
for ((key, value) in pairs) bundle.putBoolean(key, value)
}
@DslMarker
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS)
annotation class BundleDsl
/**
* Receiver scope of [buildBundle] DSL. Provides typed [with] overloads to write entries
* into the underlying [Bundle] with compile-time type safety.
*
* Marked with [BundleDsl] to prevent accidental access to outer DSL scopes from a nested
* [buildBundle] block.
*
* Cannot be instantiated outside this module — use [buildBundle].
*/
@BundleDsl
class BundleScope @PublishedApi internal constructor() {
@PublishedApi
internal val bundle = Bundle()
infix fun String.with(value: String?) = bundle.putString(this, value)
@JvmName("withInt")
infix fun String.with(value: Int) = bundle.putInt(this, value)
@JvmName("withLong")
infix fun String.with(value: Long) = bundle.putLong(this, value)
@JvmName("withBoolean")
infix fun String.with(value: Boolean) = bundle.putBoolean(this, value)
@JvmName("withBundle")
infix fun String.with(value: Bundle?) = bundle.putBundle(this, value)
@JvmName("withParcelable")
infix fun <T : Parcelable> String.with(value: T?) = bundle.putParcelable(this, value)
@JvmName("withParcelableArrayList")
infix fun <T : Parcelable> String.with(value: ArrayList<T>?) = bundle.putParcelableArrayList(this, value)
@JvmName("withSerializable")
infix fun <T : Serializable> String.with(value: T?) = bundle.putSerializable(this, value)
}
/**
* Type-safe builder for [Bundle] with mixed value types.
*
* Each `key with value` line in [block] is dispatched to the matching typed overload of
* [BundleScope.with], so passing an unsupported type (e.g. `SparseArray`) is a compile
* error rather than a runtime crash.
*
* Use [bundleOf] when all entries share the same value type.
*
* Example:
* ```
* val bundle = buildBundle {
* KEY_DISH_ID with dishId // Int
* KEY_SEARCH_USED with isNotBlank // Boolean
* KEY_SEARCH_QUERY with searchQuery // String
* }
* ```
*
* See: MOBILE-706.
*/
inline fun buildBundle(block: BundleScope.() -> Unit): Bundle = BundleScope().apply(block).bundle
@Suppress("DEPRECATION")
inline fun <reified T : Serializable> Bundle.getSerializableAs(key: String): T = getSerializable(key) as T
inline fun <reified T : Serializable> Bundle.getSerializableExtraCompat(key: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getSerializable(key, T::class.java)
} else {
@Suppress("DEPRECATION")
getSerializable(key) as T?
}
}
fun Bundle.toMap(): Map<String, Any> {
val result = mutableMapOf<String, Any>()
val keySet = keySet()
keySet.forEach { k ->
@Suppress("DEPRECATION")
val newValue: Any? = when (val value = this.get(k)) {
null -> null
is Int -> getInt(k)
is Long -> getLong(k)
is Boolean -> getBoolean(k)
is String -> getString(k)
is Byte -> getByte(k)
is ByteArray -> getByteArray(k)
is Char -> getChar(k)
is CharArray -> getCharArray(k)
is CharSequence -> getCharSequence(k)
is Float -> getFloat(k)
is FloatArray -> getFloatArray(k)
is Short -> getShort(k)
is ShortArray -> getShortArray(k)
else -> {
if (BuildConfig.DEBUG) {
error("$value is not supported")
} else {
Timber.tag(TAG).w("Bundle.toMap: unsupported value type for key=%s value=%s", k, value)
null
}
}
}
if (newValue != null) {
result[k] = newValue
}
}
return result
}
fun <V> Map<String, V>.toBundle(): Bundle {
val bundle = Bundle()
with(bundle) {
forEach { (key, value) ->
when (value) {
null -> Unit // skip null map values
is Int -> putInt(key, value)
is Long -> putLong(key, value)
is Boolean -> putBoolean(key, value)
is String -> putString(key, value)
is Byte -> putByte(key, value)
is ByteArray -> putByteArray(key, value)
is Char -> putChar(key, value)
is CharArray -> putCharArray(key, value)
is CharSequence -> putCharSequence(key, value)
is Float -> putFloat(key, value)
is FloatArray -> putFloatArray(key, value)
is Short -> putShort(key, value)
is ShortArray -> putShortArray(key, value)
else -> {
if (BuildConfig.DEBUG) {
error("$value is not supported")
} else {
Timber.tag(TAG).w("Map.toBundle: unsupported value type for key=%s value=%s", key, value)
}
}
}
}
}
return bundle
}
package ru.starksoft.core.utils.ext
import android.os.Bundle
import android.os.Parcel
import android.os.Parcelable
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.io.Serializable
@RunWith(RobolectricTestRunner::class)
@Suppress("DEPRECATION")
class BundleExtTest {
@Test
fun `buildBundle puts mixed primitive values`() {
val bundle = buildBundle {
STRING_KEY with "value"
INT_KEY with 42
LONG_KEY with 123L
BOOLEAN_KEY with true
}
assertEquals("value", bundle.getString(STRING_KEY))
assertEquals(42, bundle.getInt(INT_KEY))
assertEquals(123L, bundle.getLong(LONG_KEY))
assertTrue(bundle.getBoolean(BOOLEAN_KEY))
}
@Test
fun `buildBundle puts nullable values`() {
val bundle = buildBundle {
STRING_KEY with null as String?
BUNDLE_KEY with null as Bundle?
PARCELABLE_KEY with null as TestParcelable?
SERIALIZABLE_KEY with null as TestSerializable?
}
assertTrue(bundle.containsKey(STRING_KEY))
assertTrue(bundle.containsKey(BUNDLE_KEY))
assertTrue(bundle.containsKey(PARCELABLE_KEY))
assertTrue(bundle.containsKey(SERIALIZABLE_KEY))
assertNull(bundle.getString(STRING_KEY))
assertNull(bundle.getBundle(BUNDLE_KEY))
assertNull(bundle.getParcelable<TestParcelable>(PARCELABLE_KEY))
assertNull(bundle.getSerializable(SERIALIZABLE_KEY))
}
@Test
fun `buildBundle puts nested bundle`() {
val nestedBundle = bundleOf(STRING_KEY to "nested")
val bundle = buildBundle {
BUNDLE_KEY with nestedBundle
}
assertEquals("nested", bundle.getBundle(BUNDLE_KEY)?.getString(STRING_KEY))
}
@Test
fun `buildBundle puts parcelable value`() {
val parcelable = TestParcelable(id = 7)
val bundle = buildBundle {
PARCELABLE_KEY with parcelable
}
assertEquals(parcelable, bundle.getParcelable(PARCELABLE_KEY))
}
@Test
fun `buildBundle puts parcelable array list`() {
val parcelables = arrayListOf(TestParcelable(id = 1), TestParcelable(id = 2))
val bundle = buildBundle {
PARCELABLE_LIST_KEY with parcelables
}
assertEquals(parcelables, bundle.getParcelableArrayList<TestParcelable>(PARCELABLE_LIST_KEY))
}
@Test
fun `buildBundle puts serializable value`() {
val serializable = TestSerializable(id = 9)
val bundle = buildBundle {
SERIALIZABLE_KEY with serializable
}
assertEquals(serializable, bundle.getSerializable(SERIALIZABLE_KEY))
}
@Test
fun `buildBundle creates empty bundle when block is empty`() {
val bundle = buildBundle {}
assertFalse(bundle.containsKey(STRING_KEY))
assertTrue(bundle.isEmpty)
}
@Test
fun `buildBundle overrides previous value for same key`() {
val bundle = buildBundle {
STRING_KEY with "first"
STRING_KEY with "second"
}
assertEquals("second", bundle.getString(STRING_KEY))
assertEquals(1, bundle.size())
}
@Test
fun `buildBundle size matches put count`() {
val bundle = buildBundle {
STRING_KEY with "value"
INT_KEY with 42
LONG_KEY with 123L
BOOLEAN_KEY with true
}
assertEquals(4, bundle.size())
}
@Test
fun `bundleOf String creates bundle with all entries`() {
val bundle = bundleOf(
"a" to "first",
"b" to "second",
"c" to null,
)
assertEquals(3, bundle.size())
assertEquals("first", bundle.getString("a"))
assertEquals("second", bundle.getString("b"))
assertTrue(bundle.containsKey("c"))
assertNull(bundle.getString("c"))
}
@Test
fun `bundleOf Int creates bundle with all entries`() {
val first = 1
val second = 2
val third = 3
val bundle = bundleOf(
"a" to first,
"b" to second,
"c" to third,
)
assertEquals(3, bundle.size())
assertEquals(first, bundle.getInt("a"))
assertEquals(second, bundle.getInt("b"))
assertEquals(third, bundle.getInt("c"))
}
@Test
fun `bundleOf Long creates bundle with all entries`() {
val bundle = bundleOf(
"a" to 10L,
"b" to 20L,
)
assertEquals(2, bundle.size())
assertEquals(10L, bundle.getLong("a"))
assertEquals(20L, bundle.getLong("b"))
}
@Test
fun `bundleOf Boolean creates bundle with all entries`() {
val bundle = bundleOf(
"a" to true,
"b" to false,
)
assertEquals(2, bundle.size())
assertTrue(bundle.getBoolean("a"))
assertFalse(bundle.getBoolean("b"))
}
@Test
fun `bundleOf with no pairs creates empty bundle`() {
val stringBundle = bundleOf(*emptyArray<Pair<String, String?>>())
val intBundle = bundleOf(*emptyArray<Pair<String, Int>>())
assertTrue(stringBundle.isEmpty)
assertTrue(intBundle.isEmpty)
}
@Test
fun `buildBundle accepts Bundle EMPTY as nested bundle`() {
val bundle = buildBundle {
BUNDLE_KEY with Bundle.EMPTY
}
assertTrue(bundle.getBundle(BUNDLE_KEY)?.isEmpty == true)
}
@Test
fun `buildBundle supports all typed overloads in single block`() {
val parcelable = TestParcelable(id = 1)
val parcelables = arrayListOf(TestParcelable(id = 2))
val serializable = TestSerializable(id = 3)
val nested = bundleOf(STRING_KEY to "nested")
val bundle = buildBundle {
STRING_KEY with "value"
INT_KEY with 42
LONG_KEY with 123L
BOOLEAN_KEY with true
BUNDLE_KEY with nested
PARCELABLE_KEY with parcelable
PARCELABLE_LIST_KEY with parcelables
SERIALIZABLE_KEY with serializable
}
assertEquals(8, bundle.size())
assertEquals("value", bundle.getString(STRING_KEY))
assertEquals(42, bundle.getInt(INT_KEY))
assertEquals(123L, bundle.getLong(LONG_KEY))
assertTrue(bundle.getBoolean(BOOLEAN_KEY))
assertEquals("nested", bundle.getBundle(BUNDLE_KEY)?.getString(STRING_KEY))
assertEquals(parcelable, bundle.getParcelable(PARCELABLE_KEY))
assertEquals(parcelables, bundle.getParcelableArrayList<TestParcelable>(PARCELABLE_LIST_KEY))
assertEquals(serializable, bundle.getSerializable(SERIALIZABLE_KEY))
}
@Test
fun `buildBundle supports nested buildBundle blocks`() {
val bundle = buildBundle {
STRING_KEY with "outer"
BUNDLE_KEY with buildBundle {
STRING_KEY with "inner"
INT_KEY with 7
}
}
val inner = bundle.getBundle(BUNDLE_KEY)
assertEquals("outer", bundle.getString(STRING_KEY))
assertEquals("inner", inner?.getString(STRING_KEY))
assertEquals(7, inner?.getInt(INT_KEY))
}
@Test
fun `buildBundle survives parcel round trip`() {
val source = buildBundle {
STRING_KEY with "value"
INT_KEY with 42
LONG_KEY with 123L
BOOLEAN_KEY with true
PARCELABLE_KEY with TestParcelable(id = 5)
}
val parcel = Parcel.obtain()
source.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
val restored = parcel.readBundle(this::class.java.classLoader)
parcel.recycle()
assertEquals(5, restored?.size())
assertEquals("value", restored?.getString(STRING_KEY))
assertEquals(42, restored?.getInt(INT_KEY))
assertEquals(123L, restored?.getLong(LONG_KEY))
assertTrue(restored?.getBoolean(BOOLEAN_KEY) == true)
assertEquals(TestParcelable(id = 5), restored?.getParcelable(PARCELABLE_KEY))
}
@Test
fun `buildBundle puts empty parcelable array list`() {
val bundle = buildBundle {
PARCELABLE_LIST_KEY with arrayListOf<TestParcelable>()
}
assertTrue(bundle.containsKey(PARCELABLE_LIST_KEY))
assertEquals(emptyList<TestParcelable>(), bundle.getParcelableArrayList<TestParcelable>(PARCELABLE_LIST_KEY))
}
@Test
fun `bundleOf Int works for smart-cast from nullable`() {
val nullable: Int? = 42
// Same pattern as AnnualResultsCloseEvent: smart-cast Int? -> Int in else branch
val bundle = if (nullable == null) null else bundleOf(INT_KEY to nullable)
assertEquals(42, bundle?.getInt(INT_KEY))
}
// region toMap
@Test
fun `Bundle toMap maps all primitive types`() {
val bundle = Bundle().apply {
putInt(INT_KEY, 1)
putLong(LONG_KEY, 2L)
putBoolean(BOOLEAN_KEY, true)
putString(STRING_KEY, "value")
putFloat("float", 3.14f)
putByte("byte", 0x7F)
putChar("char", 'A')
putShort("short", 9.toShort())
}
val map = bundle.toMap()
assertEquals(1, map[INT_KEY])
assertEquals(2L, map[LONG_KEY])
assertEquals(true, map[BOOLEAN_KEY])
assertEquals("value", map[STRING_KEY])
assertEquals(3.14f, map["float"])
assertEquals(0x7F.toByte(), map["byte"])
assertEquals('A', map["char"])
assertEquals(9.toShort(), map["short"])
assertEquals(8, map.size)
}
@Test
fun `Bundle toMap maps array types`() {
val bundle = Bundle().apply {
putByteArray("byteArr", byteArrayOf(1, 2))
putCharArray("charArr", charArrayOf('a', 'b'))
putFloatArray("floatArr", floatArrayOf(1f, 2f))
putShortArray("shortArr", shortArrayOf(1, 2))
}
val map = bundle.toMap()
assertEquals(4, map.size)
assertTrue((map["byteArr"] as ByteArray).contentEquals(byteArrayOf(1, 2)))
assertTrue((map["charArr"] as CharArray).contentEquals(charArrayOf('a', 'b')))
assertTrue((map["floatArr"] as FloatArray).contentEquals(floatArrayOf(1f, 2f)))
assertTrue((map["shortArr"] as ShortArray).contentEquals(shortArrayOf(1, 2)))
}
@Test
fun `Bundle toMap returns empty map for empty bundle`() {
val map = Bundle().toMap()
assertTrue(map.isEmpty())
}
@Test
fun `Bundle toMap skips null string values`() {
val bundle = Bundle().apply {
putString(STRING_KEY, null)
putInt(INT_KEY, 1)
}
val map = bundle.toMap()
// null values are filtered out (Map<String, Any> can't hold null)
assertFalse(map.containsKey(STRING_KEY))
assertEquals(1, map[INT_KEY])
}
// endregion
// region toBundle
@Test
fun `Map toBundle puts all primitive types`() {
val source = mapOf(
INT_KEY to 1,
LONG_KEY to 2L,
BOOLEAN_KEY to true,
STRING_KEY to "value",
"float" to 3.14f,
"byte" to 0x7F.toByte(),
"char" to 'A',
"short" to 9.toShort(),
)
val bundle = source.toBundle()
assertEquals(8, bundle.size())
assertEquals(1, bundle.getInt(INT_KEY))
assertEquals(2L, bundle.getLong(LONG_KEY))
assertTrue(bundle.getBoolean(BOOLEAN_KEY))
assertEquals("value", bundle.getString(STRING_KEY))
assertEquals(3.14f, bundle.getFloat("float"))
assertEquals(0x7F.toByte(), bundle.getByte("byte"))
assertEquals('A', bundle.getChar("char"))
assertEquals(9.toShort(), bundle.getShort("short"))
}
@Test
fun `Map toBundle puts array types`() {
val source = mapOf<String, Any>(
"byteArr" to byteArrayOf(1, 2),
"charArr" to charArrayOf('a', 'b'),
"floatArr" to floatArrayOf(1f, 2f),
"shortArr" to shortArrayOf(1, 2),
)
val bundle = source.toBundle()
assertEquals(4, bundle.size())
assertTrue(bundle.getByteArray("byteArr")!!.contentEquals(byteArrayOf(1, 2)))
assertTrue(bundle.getCharArray("charArr")!!.contentEquals(charArrayOf('a', 'b')))
assertTrue(bundle.getFloatArray("floatArr")!!.contentEquals(floatArrayOf(1f, 2f)))
assertTrue(bundle.getShortArray("shortArr")!!.contentEquals(shortArrayOf(1, 2)))
}
@Test
fun `Map toBundle returns empty bundle for empty map`() {
val bundle = emptyMap<String, Any>().toBundle()
assertTrue(bundle.isEmpty)
}
@Test
fun `Bundle toMap and Map toBundle round trip`() {
val original = Bundle().apply {
putString(STRING_KEY, "value")
putInt(INT_KEY, 42)
putLong(LONG_KEY, 123L)
putBoolean(BOOLEAN_KEY, true)
}
val restored = original.toMap().toBundle()
assertEquals(4, restored.size())
assertEquals("value", restored.getString(STRING_KEY))
assertEquals(42, restored.getInt(INT_KEY))
assertEquals(123L, restored.getLong(LONG_KEY))
assertTrue(restored.getBoolean(BOOLEAN_KEY))
}
// endregion
private data class TestSerializable(val id: Int) : Serializable
private data class TestParcelable(val id: Int) : Parcelable {
constructor(parcel: Parcel) : this(parcel.readInt())
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(id)
}
override fun describeContents(): Int = 0
companion object CREATOR : Parcelable.Creator<TestParcelable> {
override fun createFromParcel(parcel: Parcel): TestParcelable = TestParcelable(parcel)
override fun newArray(size: Int): Array<TestParcelable?> = arrayOfNulls(size)
}
}
private companion object {
const val STRING_KEY = "string"
const val INT_KEY = "int"
const val LONG_KEY = "long"
const val BOOLEAN_KEY = "boolean"
const val BUNDLE_KEY = "bundle"
const val PARCELABLE_KEY = "parcelable"
const val PARCELABLE_LIST_KEY = "parcelable_list"
const val SERIALIZABLE_KEY = "serializable"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment