### List resources at runtime
```kotlin
/* Colors */
R.color::class.java.fields.forEach {
val name = it.name
val colorRes /*@ColorRes*/ = it.getInt(null)
val colorInt /*@ColorInt*/ = ContextCompat.getColor(this, colorRes)
}
/* Drawables */
R.drawable::class.java.fields.forEach {
val name = it.name
val drawableRes /*@DrawableRes*/ = it.getInt(null)
val drawable = ContextCompat.getDrawable(this, drawableRes)
}
/* Strings */
R.string::class.java.fields.forEach {
val name = it.name
val stringRes /*@StringRes*/ = it.getInt(null)
val string = getString(stringRes)
}
```
### LiveData Events
Previously known as `SingleLiveEvent` [🔗](https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150)
```kotlin
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event(private val content: T) {
var hasBeenHandled = false
private set
/**
* Returns the [content], even if it's already been handled.
*/
fun peek(): T = content
/**
* Returns the [content] and prevents its use again.
*/
fun getIfNotHandled(): T? = if (hasBeenHandled) null else content.also { hasBeenHandled = true }
/**
* Executes the [block] if the [content] has not already been handled.
*/
fun ifNotHandled(block: (T) -> Unit) = getIfNotHandled()?.let(block)
}
```
[🔗](https://github.com/android/architecture-samples/blob/todo-mvvm-live-kotlin/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/Event.kt)
```kotlin
/**
* An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s value has already been handled.
*
* [onEvent] is *only* called if the [Event]'s contents has not been handled.
*/
class EventObserver(
private val onEvent: (T) -> Unit,
) : Observer> {
override fun onChanged(event: Event) {
event.ifNotHandled(onEvent)
}
}
fun LiveData>.observeEvent(
owner: LifecycleOwner,
onEvent: (T) -> Unit,
) = observe(owner, EventObserver(onEvent))
```
[🔗](https://gist.github.com/JoseAlcerreca/e0bba240d9b3cffa258777f12e5c0ae9)
### LiveData observe non-null data
```kotlin
fun LiveData.observeNotNull(
owner: LifecycleOwner,
observer: (data: T & Any) -> Unit,
) = observe(owner) { it?.let(observer) }
```
### LiveData unit testing
```kotlin
/**
* Gets the value of a [LiveData] or waits for it to have one, with a timeout.
*
* Use this extension from host-side (JVM) tests. It's recommended to use it alongside
* `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
*/
fun LiveData.awaitValue(
duration: Duration = 2.seconds,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer {
override fun onChanged(o: T?) {
data = o
latch.countDown()
removeObserver(this)
}
}
observeForever(observer)
afterObserve.invoke()
if (!latch.await(duration.inWholeMilliseconds, MILLISECONDS)) {
removeObserver(observer)
throw TimeoutException("LiveData value was never set.")
}
@Suppress("UNCHECKED_CAST")
return data as T
}
/**
* Observes a [LiveData] until the `block` is done executing.
*/
suspend fun LiveData.test(block: suspend () -> Unit) {
val observer = Observer { }
try {
observeForever(observer)
block()
} finally {
removeObserver(observer)
}
}
```
[🔗](https://medium.com/androiddevelopers/unit-testing-livedata-and-other-common-observability-problems-bb477262eb04)
### Locate APK files
```bash
adb shell pm list packages -f
adb shell pm path
```
### Manually sign APK
```bash
apksigner sign --ks debug.keystore --key-pass pass:android --ks-key-alias androiddebugkey --ks-pass pass:android app-release.apk
```
### Multiplatform Parcelize plugin
Kotlin's [Parcelize](https://developer.android.com/kotlin/parcelize) plugin works only on Android project.
Wrapping it in a Kotlin Multiplatform project allows us to use it in JVM projects, without leaking Android implementation details.
```kotlin title="build.gradle.kts"
plugins {
kotlin("multiplatform")
id("com.android.library")
id("kotlin-parcelize")
}
android { /* ... */ }
kotlin {
android()
jvm()
sourceSets {
val commonMain by getting { /* ... */ }
val commonTest by getting { /* ... */ }
val jvmMain by getting { /* ... */ }
val jvmTest by getting { /* ... */ }
val androidMain by getting { /* ... */ }
val androidUnitTest by getting { /* ... */ }
val androidInstrumentedTest by getting { /* ... */ }
}
}
```
!!! quote ""
=== "Common"
```kotlin title="commonMain/kotlin/Parcel.kt"
expect class Parcel {
fun writeLong(long: Long)
fun readLong(): Long
}
expect interface Parcelable
```
```kotlin title="commonMain/kotlin/Parcelize.kt"
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
expect annotation class Parcelize()
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Repeatable
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
expect annotation class TypeParceler>
expect interface Parceler {
fun create(parcel: Parcel): T
fun T.write(parcel: Parcel, flags: Int)
}
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
expect annotation class IgnoredOnParcel()
```
=== "Android"
```kotlin title="androidMain/kotlin/Parcel.android.kt"
actual typealias Parcel = android.os.Parcel
actual typealias Parcelable = android.os.Parcelable
```
```kotlin title="androidMain/kotlin/Parcelize.kt"
actual typealias Parcelize = kotlinx.parcelize.Parcelize
actual typealias TypeParceler = kotlinx.parcelize.TypeParceler
actual typealias Parceler = kotlinx.parcelize.Parceler
actual typealias IgnoredOnParcel = kotlinx.parcelize.IgnoredOnParcel
```
=== "JVM"
```kotlin title="jvmMain/kotlin/Parcel.jvm.kt"
actual class Parcel {
actual fun writeLong(long: Long): Unit = TODO()
actual fun readLong(): Long = TODO()
}
actual interface Parcelable
```
```kotlin title="jvmMain/kotlin/Parcelize.kt"
actual annotation class Parcelize
actual annotation class TypeParceler>
actual interface Parceler {
actual fun create(parcel: Parcel): T
actual fun T.write(parcel: Parcel, flags: Int)
}
actual annotation class IgnoredOnParcel
```
### Parcel extensions
```kotlin title="Testing utils"
fun Parcel.use(block: (Parcel) -> T): T = try { block(this) } finally { recycle() }
context(Parceler)
fun T.parcelize(): T = Parcel.obtain().use {
write(it, 0)
it.setDataPosition(0)
create(it)
}
inline fun R.marshall(): ByteArray = Parcel.obtain().use {
it.writeValue(this)
it.marshall()
}
inline fun ByteArray.unmarshall(): R = Parcel.obtain().use {
it.unmarshall(this, 0, size)
it.setDataPosition(0)
it.readValue(R::class.java.classLoader) as R
}
```
```kotlin title="Reading boxed primitive values"
//region Read boxed primitive values
@SuppressLint("ParcelClassLoader")
fun Parcel.readIntValue(): Int? = readValue(null) as Int?
@SuppressLint("ParcelClassLoader")
fun Parcel.readLongValue(): Long? = readValue(null) as Long?
@SuppressLint("ParcelClassLoader")
fun Parcel.readFloatValue(): Float? = readValue(null) as Float?
@SuppressLint("ParcelClassLoader")
fun Parcel.readDoubleValue(): Double? = readValue(null) as Double?
@SuppressLint("ParcelClassLoader")
fun Parcel.readBooleanValue(): Boolean? = readValue(null) as Boolean?
//endregion
```
```kotlin title="Read/Write nullable values"
/**
* This will check an [Int] flag to switch between `null` and non-`null` value:
* - `0` means the value is `null` and nothing must be read
* - `1` means the value is non-`null` and can be read
*/
inline fun Parcel.readNullable(reader: Parcel.() -> T) =
if (readInt() != 0) reader() else null
/**
* This will write an [Int] flag to switch between `null` and non-`null` value:
* - `0` when the value is `null`
* - `1` when the value is non-`null`
*/
inline fun Parcel.writeNullable(value: T?, writer: Parcel.(value: T) -> Unit) {
if (value == null) return writeInt(0)
writeInt(1)
writer(value)
}
```
### Parcelize and TypeParceler
```kotlin
class Foo
object FooParceler : Parceler {
override fun create(parcel: Parcel): Foo? = TODO()
override fun Foo?.write(parcel: Parcel, flags: Int) = TODO()
}
// typealias helpers
typealias FooTypeParceler = TypeParceler
typealias FooNullableTypeParceler = TypeParceler
@Parcelize
@FooTypeParceler
@FooNullableTypeParceler
class Data(val foo: Foo, val nullableFoo: Foo?) : Parcelable
```
[🔗](https://developer.android.com/kotlin/parcelize)
### Print APK certificates
```bash
find . -name '*.apk' -type f -exec echo "APK: {}" \; -exec keytool -printcert -jarfile "{}" \;
```
### Print keystore entries
```bash
keytool -list -v -keystore ~/.android/debug.keystore
```
```
Keystore type: PKCS12
Keystore provider: SUN
Your keystore contains 1 entry
Alias name: androiddebugkey
Creation date: 24 Aug 2021
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: C=US, O=Android, CN=Android Debug
Issuer: C=US, O=Android, CN=Android Debug
Serial number: 1
Valid from: Tue Aug 24 14:21:42 CEST 2021 until: Thu Aug 17 14:21:42 CEST 2051
Certificate fingerprints:
SHA1: 9C:2B:0E:AD:D2:12:ED:68:42:00:F5:41:4A:29:A8:10:FD:4F:94:62
SHA256: 35:21:3A:55:E0:8E:2F:98:34:B6:B1:0B:13:DD:2B:25:A7:E5:9E:39:85:8C:4E:C3:F1:82:CD:33:2E:91:57:4D
Signature algorithm name: SHA1withRSA (weak)
Subject Public Key Algorithm: 2048-bit RSA key
Version: 1
```
### Project view by default
`Help` → `Edit Custom Properties` → `studio.projectview=true`
### R8 flags
- `com.android.tools.r8.printtimes`
- `com.android.tools.r8.printmemory`
[🔗](https://r8.googlesource.com/r8/+/master/src/main/java/com/android/tools/r8/utils/InternalOptions.java)
### Recursive View children sequence
```kotlin
fun View.recursiveChildren(): Sequence = sequence {
yield(this@recursiveChildren)
if (this@recursiveChildren !is ViewGroup) return@sequence
children.forEach { yieldAll(it.recursiveChildren()) }
}
```
### Resource identifier for MATCH_PARENT and WRAP_CONTENT
```xml
-1-2
```
### Resources abstraction
=== ":simple-kotlin: `BooleanResource.kt`"
```kotlin
import android.content.res.Resources
import androidx.annotation.BoolRes
sealed class BooleanResource {
companion object {
fun from(boolean: Boolean): BooleanResource = BooleanValueResource(boolean)
fun fromColorId(@BoolRes id: Int): BooleanResource = BooleanIdResource(id)
}
}
private data class BooleanValueResource(val value: Boolean) : BooleanResource()
private data class BooleanIdResource(@BoolRes val id: Int) : BooleanResource()
fun BooleanResource.toBoolean(resources: Resources): Boolean = when (this) {
is BooleanValueResource -> value
is BooleanIdResource -> resources.getBoolean(id)
}
```
=== ":simple-kotlin: `ColorResource.kt`"
```kotlin
import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.Color
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.core.content.res.ResourcesCompat
sealed class ColorResource {
companion object {
fun from(color: String): ColorResource = ColorStringResource(color)
fun fromColorId(@ColorRes id: Int): ColorResource = ColorIdResource(id)
}
}
private data class ColorStringResource(val value: String) : ColorResource()
private data class ColorIdResource(@ColorRes val id: Int) : ColorResource()
@ColorInt
fun ColorResource.toColorInt(resources: Resources, theme: Resources.Theme? = null): Int = when (this) {
is ColorStringResource -> Color.parseColor(value)
is ColorIdResource -> ResourcesCompat.getColor(resources, id, theme)
}
fun ColorResource.toColorStateList(resources: Resources, theme: Resources.Theme? = null): ColorStateList? = when (this) {
is ColorStringResource -> ColorStateList.valueOf(toColorInt(resources))
is ColorIdResource -> ResourcesCompat.getColorStateList(resources, id, theme)
}
```
=== ":simple-kotlin: `DrawableResource.kt`"
```kotlin
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toDrawable
sealed class DrawableResource {
companion object {
fun from(drawable: Drawable): DrawableResource = DrawableValueResource(drawable)
fun fromBitmap(bitmap: Bitmap): DrawableResource = DrawableBitmapResource(bitmap)
fun fromDrawableId(@DrawableRes id: Int): DrawableResource = DrawableIdResource(id)
}
}
private data class DrawableValueResource(val drawable: Drawable) : DrawableResource()
private data class DrawableBitmapResource(val bitmap: Bitmap) : DrawableResource()
private data class DrawableIdResource(@DrawableRes val id: Int) : DrawableResource()
fun DrawableResource.toDrawable(resources: Resources, theme: Resources.Theme? = null): Drawable? = when (this) {
is DrawableValueResource -> drawable
is DrawableBitmapResource -> bitmap.toDrawable(resources)
is DrawableIdResource -> ResourcesCompat.getDrawable(resources, id, theme)
}
```
=== ":simple-kotlin: `TextResource.kt`"
```kotlin
import android.content.res.Resources
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import java.text.MessageFormat
sealed class TextResource {
companion object {
fun none(): TextResource = NullTextResource
fun fromString(text: CharSequence): TextResource = StringTextResource(text)
fun fromStringId(@StringRes id: Int, vararg args: Any = emptyArray()): TextResource = StringIdTextResource(id, args.toList())
fun fromPluralId(@PluralsRes id: Int, quantity: Int, vararg args: Any = emptyArray()): TextResource = PluralIdTextResource(id, quantity, args.toList())
fun fromMessage(@StringRes id: Int, vararg args: Any = emptyArray()): TextResource = MessageIdTextResource(id, args.toList())
}
@Deprecated(
message = "Suspicious toString() usage, please use toString(resources) if you need to resolve the TextResource.",
replaceWith = ReplaceWith("toString(resources)"),
level = DeprecationLevel.WARNING
)
override fun toString(): String = super.toString()
}
private object NullTextResource : TextResource()
private data class StringTextResource(val text: CharSequence) : TextResource()
private data class StringIdTextResource(@StringRes val id: Int, val args: List) : TextResource()
private data class PluralIdTextResource(@PluralsRes val id: Int, val quantity: Int, val args: List) : TextResource()
private data class MessageIdTextResource(@StringRes val id: Int, val args: List) : TextResource()
fun TextResource?.orNone(): TextResource = this ?: TextResource.none()
fun TextResource.toText(resources: Resources): CharSequence? = when (this) {
NullTextResource -> null
is StringTextResource -> text
is StringIdTextResource -> if (args.isEmpty()) resources.getText(id) else toString(resources)
is PluralIdTextResource -> if (args.isEmpty()) resources.getQuantityText(id, quantity) else toString(resources)
is MessageIdTextResource -> if (args.isEmpty()) MessageFormat.format(resources.getString(id)) else toString(resources)
}
fun TextResource.toString(resources: Resources): String? = when (this) {
NullTextResource -> null
is StringTextResource -> text.toString()
is StringIdTextResource -> if (args.isEmpty()) resources.getString(id) else resources.getString(id, *args.mapTextResourceToString(resources))
is PluralIdTextResource -> if (args.isEmpty()) resources.getQuantityString(id, quantity) else resources.getQuantityString(id, quantity, *args.mapTextResourceToString(resources))
is MessageIdTextResource -> if (args.isEmpty()) MessageFormat.format(resources.getString(id)) else MessageFormat.format(resources.getString(id), *args.mapTextResourceToString(resources))
}
private fun List.mapTextResourceToString(resources: Resources) = map { if (it is TextResource) it.toString(resources) else it }.toTypedArray()
```
### Robolectric fake Browser Activity
```kotlin
import org.robolectric.RuntimeEnvironment.getApplication
import org.robolectric.Shadows.shadowOf
fun addFakeWebBrowserActivity() = shadowOf(getApplication().packageManager).apply {
with(ComponentName(getApplication().packageName, "FakeBrowserActivity")) {
addActivityIfNotPresent(this)
addIntentFilterForActivity(
this,
IntentFilter(Intent.ACTION_VIEW).apply {
addDataScheme("https")
addCategory(Intent.CATEGORY_DEFAULT)
addCategory(Intent.CATEGORY_BROWSABLE)
},
)
}
}
```
### Run command as a specific application user-id
```bash
adb shell run-as []
```
### screencap
```bash
adb shell screencap -p "/sdcard/screencap.png" && adb pull "/sdcard/screencap.png" "$USER/Desktop/%time::=-%%random%.png" && adb shell rm "/sdcard/screencap.png"
```
```pwsh
%comspec% /c adb shell screencap -p "/sdcard/screencap.png" && adb pull "/sdcard/screencap.png" "%UserProfile%\Desktop\%time::=-%%random%.png" && adb shell rm "/sdcard/screencap.png"
```
### scrcpy
[GitHub](https://github.com/Genymobile/scrcpy)
- record
```bash
scrcpy -m 1080 --show-touches --no-display --record "$USER/Desktop/%time::=-%%random%.mp4"
```
```pwsh
%comspec% /c scrcpy -m 1080 --show-touches --no-display --record "%UserProfile%\Desktop\%time::=-%%random%.mp4"
```
- mirror
```bash
scrcpy -m 1080 --show-touches
```
```pwsh
%comspec% /c scrcpy -m 1080 --show-touches
```
### Step by step installation
```bash
# Installing OpenJDK
# https://openjdk.org/install/
sudo apt-get update
sudo apt-get install openjdk-17-jdk
# Installing Android Command Line Tools
# https://developer.android.com/studio#command-line-tools-only
cd ~
curl https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -o /tmp/cmd-tools.zip
mkdir -p android/cmdline-tools
unzip -q -d android/cmdline-tools /tmp/cmd-tools.zip
mv android/cmdline-tools/cmdline-tools android/cmdline-tools/latest
rm /tmp/cmd-tools.zip
# Setting up environment variables
export ANDROID_HOME=$HOME/android
export ANDROID_SDK_ROOT=${ANDROID_HOME}
export PATH=${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${PATH}
# Accepting SDK licenses
yes | sdkmanager --licenses
# Installing SDK components
sdkmanager --update
sdkmanager --list
sdkmanager --list_installed
sdkmanager --install "platforms;android-33"
```
### Strict Modes
```kotlin
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyFlashScreen()
.penaltyLog()
.build()
.let(StrictMode::setThreadPolicy)
StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
.let(StrictMode::setVmPolicy)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.detectFragmentReuse()
.detectFragmentTagUsage()
.detectRetainInstanceUsage()
.detectTargetFragmentUsage()
.detectWrongFragmentContainer()
.detectSetUserVisibleHint()
.penaltyLog().build()
```
### Suppress unsupported options
```properties title="gradle.properties"
android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,\
android.enableR8.fullMode,\
android.enableUnitTestBinaryResources,\
...
```
### Tools sample resources as JSON files
```json title="./sampledata/users.json"
{
"data": [
{
"name": "Mike Barrett",
"address": "4898 Locust Rd",
"avatar": "@sample/avatars"
},
{
"name": "Kylie Long",
"address": "2659 Taylor St",
"avatar": "@sample/avatars"
}
]
}
```
```xml
```
[🔗 `@tools:sample/*`](https://developer.android.com/studio/write/tool-attributes#toolssample_resources)
### Uninstall apps by package name
```bash
adb shell pm list packages `: # List packages` |
cut -d ':' -f2 `: # Extract package name` |
grep ^com. `: # Optional pattern` |
xargs --verbose -n1 adb uninstall
```
Or with fzf
```bash
adb shell pm list packages `: # List packages` |
cut -d ':' -f2 `: # Extract package name` |
fzf --header '📦 Select one or more packages to uninstall' --border --reverse --multi |
xargs --no-run-if-empty --verbose -n1 adb uninstall
```
### USSD and secret codes
These codes must be typed in the dialer app.
- `*#06#`: IMEI
- `*#*#4636#*#*`: Device info
- `*#*#7780#*#*`: Factory reset ♻️
I've created [Secret Codes](https://github.com/SimonMarquis/Android-SecretCodes) to help you find theses codes directly on your device.
[🔗](https://cs.android.com/search?q=android_secret_code)
### ViewBinding extension
```kotlin
fun AppCompatActivity.setContentView(
inflate: (LayoutInflater) -> Binding
): Binding = inflate(layoutInflater).also {
setContentView(it.root)
}
```
```kotlin
private lateinit var binding: MainViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = setContentView(MainViewBinding::inflate)
}
```
### ViewBinding one-liner
```kotlin
private lateinit var binding: MainViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(MainViewBinding.inflate(layoutInflater).also { binding = it }.root)
}
```
### ViewModel in custom View
```kotlin
class CustomView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
private val viewModel by lazy { ViewModelProvider(findViewTreeViewModelStoreOwner()!!).get() }
override fun onAttachedToWindow() {
super.onAttachedToWindow()
viewModel.state.observe(findViewTreeLifecycleOwner()!!) { TODO() }
}
}
```
### WebView Safe Browsing
```xml
```
[🔗](https://android-developers.googleblog.com/2017/06/whats-new-in-webview-security.html)
### WorkManager diagnostic
[🔗](https://developer.android.com/topic/libraries/architecture/workmanager/how-to/debugging#request-diagnostic-information-from-workmanager-2.4.0) This provides information on:
- Work requests that were completed in the last 24 hours.
- Work requests that are currently running.
- Scheduled work requests.
```bash
adb shell am broadcast -a "androidx.work.diagnostics.REQUEST_DIAGNOSTICS" -p my.package.name
```