FeedbackJar
·

How to Add an In-App Feature Board to an Android App (Kotlin)

Add a native feature-request board to an Android app: Maven dependency, one init call, Views or Compose UI. Votes, comments, device metadata. Same board as iOS.

android sdk kotlin compose in-app feedback feature voting wishkit alternative

How to add an in-app feature board to an Android app

Play reviews are not a backlog.

A user writes “add dark mode” as a 2-star. You cannot merge it with the eleven other people who asked. You cannot tell them you shipped. You cannot see that three of them are on the Pro plan.

The fix is a board inside the app: list, vote, comment, submit. One tap from Settings. Not a Chrome Custom Tab of your Canny page. Not a WebView that needs SSO before it renders.

This is the Android version. One Maven artifact, one init, one screen. Same widget ID as the SwiftUI board so iOS and Play share one vote count.

SDK: github.com/feedbackjar/kotlin-sdk
Min SDK: 21 (Android 5.0)
Artifact: com.feedbackjar:sdk:1.5.1

If you landed here because WishKit has no Android package: WishKit alternative for Android.


What you are adding

A native screen that can:

  • List public requests
  • Let a guest upvote without creating an account
  • Open a thread and comment
  • Submit a request with Android version, device model, screen size, app version, and locale attached

Prebuilt UI ships in two flavours: framework Views (no Compose, no Material pulled in) and a Compose board that only resolves if your app already uses Compose. You can also call submit / listFeedback / vote from your own layout.


Step 1 — Get a widget ID

  1. Start a 7-day trial (no card).
  2. Create a project.
  3. Copy the widget ID from the dashboard.

That ID is what the SDK sends on every request. Reuse it in the Swift package if you also ship iOS.


Step 2 — Add the dependency

Maven Central. No extra repo for most projects.

settings.gradle.kts:

kotlin
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}

build.gradle.kts (app module):

kotlin
dependencies {
    implementation("com.feedbackjar:sdk:1.5.1")
}

The AAR declares INTERNET. You do not add it to your manifest.


Step 3 — Init once

Do this before any screen that touches the board. Application.onCreate() is enough.

kotlin
import com.feedbackjar.sdk.FeedbackJar

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        FeedbackJar.init(this, "your-widget-id")
    }
}

Register MyApp in the manifest if it is not already. Wrong widget ID = empty board or failed submits.


Step 4 — Present the board

Views (zero extra UI libraries)

kotlin
import com.feedbackjar.sdk.ui.FeedbackJarActivity
import com.feedbackjar.sdk.ui.FeedbackJarView

// Full-screen activity shipped by the SDK
startActivity(FeedbackJarActivity.intent(this))

// Or embed
val board = FeedbackJarView(this)
board.setAccentColor(Color.parseColor("#e5484d"))
setContentView(board)

If you embed FeedbackJarView, give it back-stack control:

kotlin
override fun onBackPressed() {
    if (!board.onBackPressed()) super.onBackPressed()
}

Compose (only if the app already uses Compose)

The published AAR does not force Compose on you. The composable compiles only in a Compose app.

kotlin
import androidx.compose.ui.graphics.Color
import com.feedbackjar.sdk.ui.compose.FeedbackJarBoard

FeedbackJarBoard(
    accentColor = Color(0xFFE5484D),
    boardId = "board-id" // optional
)

The board follows light/dark, hides votes or comments when those are off in the dashboard, and never throws. Failures show the server message inline.


Step 5 — Put it where people will open it

Settings row. Default. Label it “Feature requests” or “Feedback.” Not “Ideas.”

kotlin
// In your Settings list
row.setOnClickListener {
    startActivity(FeedbackJarActivity.intent(this))
}

Compose Settings:

kotlin
ListItem(
    headlineContent = { Text("Feature requests") },
    leadingContent = { Icon(Icons.Outlined.Lightbulb, contentDescription = null) },
    modifier = Modifier.clickable { showBoard = true }
)

Tab only if feedback is a core loop. A fourth tab nobody asked for is clutter.

Do not hide it behind shake-only. Shake is for crashes. Feature requests need a row a human can find on purpose.


Step 6 — Tell the SDK who the user is

Anonymous voting works without this. Each install gets a random id in SharedPreferences (not advertising id, not SSAID). Reinstall or clear-data resets it.

If the user is signed in:

kotlin
FeedbackJar.setIdentity(name = user.name, email = user.email)

On logout:

kotlin
FeedbackJar.clearIdentity()

Attach plan or flavor when you submit from your own form:

kotlin
lifecycleScope.launch {
    FeedbackJar.submit(
        userText,
        properties = mapOf(
            "plan" to "pro",
            "flavor" to BuildConfig.FLAVOR
        )
    )
}

Values: String, Number, Boolean. No nested maps. These merge into the auto-collected packageName, versionName, versionCode.

Callback form if you are not in a coroutine:

kotlin
FeedbackJar.submit(userText) { result ->
    result.onSuccess { response -> /* response.postId */ }
    result.onFailure { error -> /* show error.text */ }
}

Rate limit: 5 submits per 15 minutes per IP. Handle failure in the UI.


Step 7 — Test it like a user

  1. Run on an emulator or device.
  2. Seed two real requests in the dashboard first. An empty board looks broken.
  3. Open the board from Settings. Submit “test from emulator.”
  4. Confirm the post in the FeedbackJar dashboard. Check Android version, model, app version.
  5. Upvote from a second emulator (different install id). Count should move.
  6. Change status in the dashboard, reopen the board.

If submit fails: widget ID, network, or the rate limit. The UI prints the server text.

Docs: Android SDK.


Custom UI (only if you need it)

kotlin
lifecycleScope.launch {
    val page = FeedbackJar.listFeedback(limit = 20).getOrNull()
    page?.posts?.forEach { post ->
        // post.title, post.upvotes, post.status, post.hasVoted
    }

    FeedbackJar.vote(post.id)
    FeedbackJar.unvote(post.id)
}

Read dashboard flags before you draw fields:

kotlin
val config = FeedbackJar.getConfig().getOrNull()
showNameField  = config?.collectName == true
showEmailField = config?.collectEmail == true
showVoteButton = config?.allowVotes == true
showComments   = config?.allowComments == true

What this is not

  • Not a Canny / Featurebase / Nolt WebView. Those need SSO or cookie hacks. Does Canny have a mobile SDK?
  • Not WishKit. WishKit has no Android artifact. WishKit alternative
  • Not Instabug. Shake-to-report is a crash tool. This is a voting board.
  • Not a second backlog. Same widget ID on iOS.

After it is live

Connect MCP so Cursor / Claude Code can list top-voted posts, read OS + app version, mark Done, and notify voters.

A board that stays on Planned trains people to leave Play reviews instead.


Copy-paste checklist

  • Trial account, widget ID copied
  • implementation("com.feedbackjar:sdk:1.5.1")
  • FeedbackJar.init(this, widgetId) in Application
  • Settings row → FeedbackJarActivity.intent(this) or FeedbackJarBoard()
  • setIdentity after login
  • Two seed posts so the board is not empty
  • One test submit, metadata visible in the dashboard

Start a 7-day trial — no card. Same widget ID works on Android and iOS.