What you get
Live edit works like Android Studio's Database Inspector, but from your browser. SQLite Viewer sends each change to a tiny ContentProvider inside your debug app, which runs it on the app's own database. The app keeps running, and nothing has to be stopped, copied or restarted.
Debug builds only. The helper lets anyone with USB debugging access run write statements on the database, so register it only in
src/debug and never ship it in a release build.Set it up
- Add the helper class. Download SqliteViewerProvider.kt (or copy the code below) into your app module, for example
app/src/main/java/com/example/debughelper/. Change thepackageline to match the folder. - Register it for debug builds only. Create
app/src/debug/AndroidManifest.xmlwith the provider entry below. Because it lives in thedebugsource set, release builds never contain the entry and nothing is exposed. - Optional: refresh Room screens. Room only notices changes made through its own connection. Set the
afterWritehook once so yourLiveDataandFlowscreens update right after a live edit. - Run the debug build. Install your debug build on the phone and open the app once so Android registers the provider.
- Turn on Live edit. In SQLite Viewer choose Android, connect your phone, open the app database and tick Live edit. A green LIVE EDIT badge appears. Now every row edit, insert, delete and SQL statement you run is also applied to the running app.
Debug manifest
<?xml version="1.0" encoding="utf-8"?>
<!-- src/debug/AndroidManifest.xml (debug builds only) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<provider
android:name="com.example.debughelper.SqliteViewerProvider"
android:authorities="${applicationId}.sqliteviewer"
android:exported="true" />
</application>
</manifest>Optional Room hook
// In your Application class (optional, only if you use Room LiveData / Flow)
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
SqliteViewerProvider.afterWrite = {
AppDatabase.getInstance(this).invalidationTracker.refreshVersionsAsync()
}
}
}The helper (SqliteViewerProvider.kt)
package com.example.debughelper // use any package, for example your app's own
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.net.Uri
import android.os.Bundle
import android.util.Base64
import org.json.JSONArray
/**
* DEBUG BUILDS ONLY.
*
* Lets SQLite Viewer (https://sqliteviewer.codemasterlk.com) edit this app's databases
* while the app keeps running. The class can live in src/main, but it is only registered
* in src/debug/AndroidManifest.xml, so release builds never expose it.
*/
class SqliteViewerProvider : ContentProvider() {
companion object {
/** Optional: called after every successful live edit, e.g. to refresh Room observers. */
@JvmStatic
var afterWrite: (() -> Unit)? = null
private val allowedStatement = Regex("""^(INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|ALTER)\b""", RegexOption.IGNORE_CASE)
private val leadingNoise = Regex("""^(\s|--[^\n]*\n|/\*.*?\*/)*""", RegexOption.DOT_MATCHES_ALL)
private val safeName = Regex("""^[A-Za-z0-9._-]+$""")
}
private val handles = HashMap<String, SQLiteDatabase>()
override fun onCreate(): Boolean = true
override fun call(method: String, arg: String?, extras: Bundle?): Bundle {
val answer = try {
when (method) {
"ping" -> "OK:1"
"batch" -> "OK:" + runBatch(extras?.getString("db"), arg)
else -> "ERR:unknown method $method"
}
} catch (e: Throwable) {
"ERR:" + (e.message ?: e.javaClass.simpleName).replace('\n', ' ')
}
return Bundle().apply { putString("r", answer) }
}
@Synchronized
private fun database(name: String): SQLiteDatabase {
handles[name]?.let { if (it.isOpen) return it }
val file = context!!.getDatabasePath(name)
require(file.exists()) { "database not found: $name" }
return SQLiteDatabase.openDatabase(file.path, null, SQLiteDatabase.OPEN_READWRITE)
.also { handles[name] = it }
}
private fun runBatch(name: String?, base64: String?): Int {
require(name != null && safeName.matches(name)) { "invalid database name" }
require(!base64.isNullOrEmpty()) { "no statements" }
val statements = JSONArray(String(Base64.decode(base64, Base64.DEFAULT), Charsets.UTF_8))
val db = database(name)
db.beginTransaction()
try {
for (i in 0 until statements.length()) {
val sql = statements.getString(i)
require(allowedStatement.containsMatchIn(sql.replaceFirst(leadingNoise, ""))) {
"statement type not allowed"
}
db.execSQL(sql)
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
afterWrite?.invoke()
return statements.length()
}
// Not used: the viewer talks to this provider only through call().
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? = null
override fun getType(uri: Uri): String? = null
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int = 0
}
What it can and cannot do
- It runs
INSERT,UPDATE,DELETE,REPLACE,CREATE,DROPandALTERstatements. Reads, PRAGMAs andVACUUMare refused. - Each edit is applied in a transaction. If the app rejects it, SQLite Viewer shows the error and reloads the data from the phone so both stay in sync.
- Very large edits (about 75 KB of SQL) are not sent live. Use Push to phone for those. It replaces the whole database file and restarts the app.
- The database name must match the file in the app's
databasesfolder. The helper uses the debug app's package name, so it works withapplicationIdSuffixtoo. - Works with Android Studio running at the same time when you use the ADB server mode of the Android panel.
Troubleshooting
- "Live-edit helper not found": the debug manifest entry is missing, the app was not reinstalled, or the package name differs. The provider authority must be
<your package>.sqliteviewer. - "database not found": open the app once so it creates its database, then try again.
- Screens do not update: add the optional Room hook above, or reopen the screen.