{"id":19509879,"url":"https://github.com/steamclock/debug-menu-android","last_synced_at":"2025-06-22T17:03:53.753Z","repository":{"id":43004163,"uuid":"435651127","full_name":"steamclock/debug-menu-android","owner":"steamclock","description":null,"archived":false,"fork":false,"pushed_at":"2024-02-12T23:16:39.000Z","size":402,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":10,"default_branch":"main","last_synced_at":"2025-01-08T11:46:30.524Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/steamclock.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2021-12-06T21:20:16.000Z","updated_at":"2021-12-22T21:20:03.000Z","dependencies_parsed_at":"2024-01-24T18:48:04.543Z","dependency_job_id":null,"html_url":"https://github.com/steamclock/debug-menu-android","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steamclock%2Fdebug-menu-android","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steamclock%2Fdebug-menu-android/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steamclock%2Fdebug-menu-android/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steamclock%2Fdebug-menu-android/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/steamclock","download_url":"https://codeload.github.com/steamclock/debug-menu-android/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240761122,"owners_count":19853255,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-10T23:13:47.683Z","updated_at":"2025-02-25T22:45:12.226Z","avatar_url":"https://github.com/steamclock.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# debug-menu-android\nA simple way to deploy a password-protected debug menu to your app.\n\n\nThis repo includes 5 modules:\n\n**app** - A sample app demonstrating usage of the menu.\n\n**debugmenu-core** - The core framework for the debug menu, written in pure Kotlin\n\n**debugmenu-sharedprefs** - A persistence layer for debugmenu that can be used on Android, based on Datastore Preferences\n\n**debugmenu-ui** - A UI layer for debugmenu that can be used on Android, written in Jetpack Compose and hosted in a DialogFragment.\n\n**debugmenu-codegen** - The annotations and annotation processor used for compile-time safe usage of the menu.\n\n![](3.png)\n![](1.png)\n\n![](4.png)\n![](2.png)\n\n# Installation\n[![](https://jitpack.io/v/steamclock/debug-menu-android.svg)](https://jitpack.io/#steamclock/debug-menu-android)\n\n1. Add this in your **root** build.gradle at the end of repositories:\n```gradle\nallprojects {\n    repositories {\n        // other repositories\n        maven { url 'https://jitpack.io' }\n    }\n}\n``` \n\n2. Add dependencies in your app module's build.gradle:\n```gradle\n// only necessary if you're using the annotations\nplugins {\n    id 'kotlin-kapt'\n}\n\ndependencies {\n    implementation \"com.github.steamclock.debug-menu-android:core:\u003cVERSION\u003e\"\n    implementation \"com.github.steamclock.debug-menu-android:sharedprefs:\u003cVERSION\u003e\"\n    implementation \"com.github.steamclock.debug-menu-android:compose:\u003cVERSION\u003e\"\n\n    // these two dependencies are only necessary if you're using the annotations\n    kapt \"com.github.steamclock.debug-menu-android:codegen:\u003cVERSION\u003e\"\n    implementation \"com.github.steamclock.debug-menu-android:annotation:\u003cVERSION\u003e\"\n}\n```\nMost recent version can be found [here](https://github.com/steamclock/debug-menu-android/releases)\n\n3. Sync your project gradle files\n\n4. DebugMenu should now be available in the project.\n\n# Usage\n## Initialization\n\nIn order to initialize the DebugMenu, you'll need to generate the SHA-256 encoding for a passcode of your choosing, then enter that in the DebugMenu initialization:\n```kotlin\nclass App: Application() {\n    override fun onCreate() {\n        super.onCreate()\n        DebugMenu.initialize(\"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8\",\n            ComposeDebugMenuDisplay(this),\n            SharedPrefsPersistence(this),\n            header = \"\", // optional header to display on all debug menus\n            footer = \"\" // optional footer to display on all debug menus\n        )\n    }\n}\n```\n\nFor the example above, I used https://emn178.github.io/online-tools/sha256.html to enter `password`.\nFrom here, you're ready to start adding options.\n\nCurrently the following options are supported:\n| `DebugOption`    | UI Shown | Default Value |\n|-------------------|-------------------------------------------|-------|\n| `BooleanValue`    | A true/false toggle                       | `false` |\n| `IntValue`        | An input field for integer values         | `0`     |\n| `DoubleValue`     | An input field for double values          | `0.0`   |\n| `LongValue`       | An input field for long values            | `0L`    |\n| `Action`          | A button that will call the code provided | n/a   |\n| `OptionSelection` | A drop-down menu of string values         | `null`  |\n| `TextDisplay`     | A string, useful for showing information  | n/a  |\n\n## Manual Usage\n### Adding Options\n\nYou can add an option to the menu at runtime using \n`DebugMenu.instance.addOptions(vararg newOptions: DebugOption)` \nor\n`DebugMenu.instance.addOptions(menuKey: String, vararg newOptions: DebugOption)`.\n\nSpecifying a `menuKey` will allow you to split debug options into separate menus as the app grows.\n\n```kotlin\nclass MainActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        runBlocking {\n            DebugMenu.instance.addOptions(\n                BooleanValue(\n                    title = \"Test toggle\", // The title shown in the debug menu\n                    key = \"test-toggle\", // the key used to persist this value,\n                    defaultValue = false // optional param\n                ),\n                Action(\n                    title = \"Test action\",\n                    onClick = {\n                        // note, Action.onClick is a suspending function, \n                        // so we can invoke other suspending functions here\n                        testAction()\n                    }\n                ),\n                OptionSelection(\n                    title = \"Test Selection\",\n                    key = \"test-selection\",\n                    options = listOf(\"value1\", \"value2\"),\n                    defaultIndex = null\n                ),\n                TextDisplay(\n                    text = \"Test display text\"\n                )\n            )    \n        }\n    }\n}\n```\n\n### Reading Debug Values\nDebugMenu provides options as either a Kotlin Flow, with some convenience functions for reading a single value.\n\n```kotlin\n// As a Flow\nDebugMenu.instance.flow\u003cBoolean\u003e(\"test-toggle\").collectLatest { \n    // use latest test-toggle value\n}\nDebugMenu.instance.flow\u003cBoolean\u003e(\"test-toggle\").collectAsState(initial = false) // for use in Jetpack Compose\n\n// As a single value\nDebugMenu.instance.value\u003cBoolean\u003e(key = \"test-toggle\") // suspending function\nDebugMenu.instance.valueBlocking\u003cBoolean\u003e(key = \"test-toggle\") // blocking function\n```\n\n`OptionSelection`'s selected index is stored as an `Int`, so you'll need one extra step to read the value:\n\n```kotlin\n// Retrieve the option\nval option = DebugMenu.instance.optionForKey(\"test-selection\") as OptionSelection\n\n// As a Flow, map the selected-index flow to the option, creating a Flow\u003cString\u003e for the actual value\nval valueFlow: Flow\u003cString\u003e = DebugMenu.instance.flow\u003cInt\u003e(\"test-selection\").mapLatest { selected -\u003e\n    option.options[selected]\n}\n\n// read index as a single value\nval index: Int? = DebugMenu.instance.value(\"test-selection\") // suspending\nval index: Int? = DebugMenu.instance.valueBlocking(\"test-selection\") // blocking\n\n// handle index == null case before usage\noption.options[index]\n```\n\nBecause manually adding options requires the use of abitrary Strings, it's a good idea to define constants for the values. This is also a good idea for `menuKey`s.\n\n```kotlin\nobject Debug {\n  object Menu {\n      const val loginMenu = \"LoginMenu\"\n  }\n  \n  object Key {\n      const val testToggle = \"test-toggle\"\n  }\n}\n```\n\n## Annotations Usage\nUsing the `debugmenu-codegen` library allows you to use some custom annotations for a number of benefits:\n- Reducing boilerplate code\n- Compile-time option additions\n- Compile-time safe menu usage\n\nBecause the all annotated menu options are added at compile time, it's recommended that you use separate `menuKey`s for related options.\n\n### Adding Options\nAdding options can be done using the following Annotations:\n\n| Annotation   | `BooleanValue`    | UI Shown                       |\n|----------------------|-------------------|-------------------------------------------|\n| `@DebugBoolean`      | `BooleanValue`    | A true/false toggle                       |\n| `@DebugInt`          | `IntValue`        | An input field for integer values         |\n| `@DebugDouble`       | `DoubleValue`     | An input field for double values          |\n| `@DebugLong`         | `LongValue`       | An input field for long values            |\n| `@DebugAction`       | `Action`          | A button that will call the code provided |\n| `@DebugSelection`    | `OptionSelection` | A drop-down menu of string values         |\n| `@DebugTextProvider` | `TextDisplay`      | A string, useful for showing information |\n| `@DebugSelectionProvider` | `OptionSelection`      | A drop-down menu of string values |\n\nAll annotations support optional `defaultValues` and `menuKey` parameters.\n\n```kotlin\n@DebugBoolean(title = \"Test toggle\")\nobject TestToggle\n\n@DebugAction(title = \"Global test action\")\nfun globalTestAction() {\n  // can only access global state\n}\n\n@DebugSelection(title = \"Test Selection\", options = [\"value1\", \"value2\"])\nobject TestSelection\n\nclass MainActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n    }\n}\n```\n\nIt's also possible to define an action scoped to a class using `@DebugAction`.\n```kotlin\nclass MainActivity : AppCompatActivity() {\n    @DebugAction(title = \"Scoped action\")\n    fun scopedGlobalAction() {\n        // can access MainActivity's state, is called when the onClick for the debug button is invoked \n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        initDebugMenus() // generated extension function that must be called for scoped actions to appear\n    }\n}\n```\n\nYou can also provide a text provider, scoped to a class using `@DebugTextProvider`. `@DebugTextProvider` is currently only available when scoped to a class.\n```kotlin\nclass MainActivity : AppCompatActivity() {\n    @DebugTextProvider\n    fun textProvider(): String {\n        // string generated when `initDebugMenus()` is called\n        return \"Simple text provider\"\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        initDebugMenus() // generated extension function that must be called for scoped actions to appear\n    }\n}\n```\n\n\nYou can also define an option selection provider, scoped to a class using `@DebugSelectionProvider`. `@DebugSelectionProvider` is currently only available when scoped to a class.\n```kotlin\nclass MainActivity : AppCompatActivity() {\n    @DebugSelectionProvider\n    fun textProvider(): List\u003cString\u003e {\n        // list of options generated when `initDebugMenus()` is called\n        return listOf( \n            \"Option 1\",\n            \"Option 2\",\n        )\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        initDebugMenus() // generated extension function that must be called for scoped actions to appear\n    }\n}\n```\n\n## Reading Debug Values\nDebug values can be used in a compile-time safe way using the automatically generated classes.\nEach `menuKey` included in an annotation will generate a class of the same name, which will include a number of wrappers for the options included in that menu. If a `menuKey` is not provided, the debug option will be added to the `GlobalDebugMenu` object instead.\n\nExample:\n```kotlin\n@DebugBoolean(menuKey = \"ToggleMenu\", title = \"Test toggle\")\nobject TestToggle\n\n@DebugSelection(menuKey = \"SelectionMenu\", title = \"Test Selection\", options = [\"value1\", \"value2\"])\nobject TestSelection\n```\n\nWill generate code that allows us to use the following:\n```kotlin\n// Flow\nToggleMenu.testToggle.flow.collectAsState(initial = false)\n// Annotation generated test selections are automatically converted from Flow\u003cInt\u003e -\u003e Flow\u003cString\u003e, so you can use the value directly\nTestSelection.testSelection.flow.collectAsState(initial = null)\n\n// Single value\nToggleMenu.testToggle.value() // suspending\nToggleMenu.testToggle.blockingValue() // blocking\n\nToggleMenu.testSelection.value() // suspending, returns String value\nToggleMenu.testSelection.blockingValue() // blocking, returns String value\n```\n\n## Displaying the Menu\nAny attempt to show the DebugMenu will prompt the user to enter a password if they haven't done so previously.\n\nThe `debugmenu-ui` provides extension functions for displaying the menu with a gesture in both Jetpack Compose and Android Views:\n```kotlin\n// Jetpack Compose\nText(\n  text = \"Build Info\", \n  modifier = Modifier.showDebugMenuOnGesture(menuKey = \"testing-menu\", longPressDuration = 5000L)\n)\n\n// View\nval textView: TextView = findViewById(R.id.buildInfo)\ntextView.showDebugMenuOnGesture(menuKey = \"testing-menu\", longPressDuration = 5000L)\n```\n\nNote that for the compose modifier function, it will disable interactivity on normal click behaviour within the component. For something like a `Button`, we've provided an optional `onClick` method to the `showDebugMenuOnGesture` function for passing the onClick through.\n\nThe DebugMenu can also be shown using the current instance's `show` method, optionally providing the key for the menu you want to show (otherwise, the global menu is used). This can be useful for building a top-level menu that can show more specific menus.\n```kotlin\nDebugMenu.instance.show(menu = \"testing-menu\")\nOR\nDebugMenu.instance.show()\n```\n\nNote that for menus that have debug values added via annotations, you can use the generated class versions:\n```kotlin\nTestingMenu.show()\nGlobalDebugMenu.show()\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsteamclock%2Fdebug-menu-android","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsteamclock%2Fdebug-menu-android","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsteamclock%2Fdebug-menu-android/lists"}