FeedbackJar

Android (Kotlin)

The FeedbackJar Android SDK is a data layer plus an optional prebuilt board. Use just the data layer if you’re building your own UI, or drop in FeedbackJarView / FeedbackJarBoard for a complete list → detail → new feedback flow with voting and comments already wired up.

Coming from an iOS-only tool like WishKit? See the WishKit alternative for Android for a side-by-side comparison.

Installation

Add Maven Central (most projects already have it) and the dependency.

settings.gradle.kts:

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

build.gradle.kts (app module):

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

The SDK declares the INTERNET permission itself — nothing to add to your manifest.

Setup

Initialize once before use — typically in your Application.onCreate():

kotlin
import com.feedbackjar.sdk.FeedbackJar

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

Get your widget ID from Settings → Widgets in your FeedbackJar project.

Prebuilt board (optional)

Skip building your own screens — the SDK ships a complete board on both view systems.

Views — no RecyclerView, no ConstraintLayout, no Material, no other AndroidX UI dependency:

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

// Full-screen, register-free
startActivity(FeedbackJarActivity.intent(this))
// optional accent:
startActivity(FeedbackJarActivity.intent(this, Color.parseColor("#e5484d")))

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

Compose — compileOnly in the published .aar, so it only resolves if your app already uses Compose:

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

FeedbackJarBoard(
    accentColor = Color(0xFFE5484D), // optional, defaults to FeedbackJar red
    boardId = "board-id",            // optional: restrict to one board
)

Either one gives you the full loop — list, upvote, detail, comments, and a new-feedback form — built only on the API methods below. Nothing throws; failures render inline.

Identify the submitter (optional)

Remember a name/email so later submit, vote, and addComment calls attribute to the same person, and so their name/email are synced to the server against this install’s anonymous id:

kotlin
FeedbackJar.setIdentity(name = user.name, email = user.email)
val identity = FeedbackJar.getIdentity()

// On logout:
FeedbackJar.clearIdentity()

Skip this and submissions stay fully anonymous — setIdentity is optional, not required.

Verified identity (signed-in users)

If your app has its own signed-in users, attribute their votes/comments to a real, verifiable account — consistent across every device they use — instead of the unverified name/email above. Your backend computes the signature:

kotlin
// Your backend, once the user is authenticated:
//   timestamp = System.currentTimeMillis()
//   signature = HMAC-SHA256(orgSecretKey, "$userId:$email:$timestamp")
// (org secret key is in the FeedbackJar dashboard)

FeedbackJar.setIdentity(
    userId = user.id,
    email = user.email,
    signature = signatureFromYourBackend,
    timestamp = timestampFromYourBackend,
)

Once set, vote, addComment, submit, and hasVoted all attach to this real user on every device — immediately, and any votes/comments already made anonymously on this device are folded in automatically. timestamp must be the exact value your backend signed, and expires after 7 days — call setIdentity again on each sign-in.

Submit feedback

Each submission automatically includes device metadata (Android version, device model, screen size, app version, locale).

kotlin
lifecycleScope.launch {
    val result = FeedbackJar.submit(userText)
    result
        .onSuccess { response ->
            Log.d("FeedbackJar", "Submitted: ${response.postId}")
        }
        .onFailure { error ->
            Log.e("FeedbackJar", "Failed to submit", error)
        }
}

A callback variant is available when you are not using coroutines:

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

Pass email/userName for a headless submission where identity isn’t already known, and properties for your own key/value context merged into the auto-collected app metadata:

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

List feedback

Fetch the public feedback feed with optional pagination:

kotlin
lifecycleScope.launch {
    val result = FeedbackJar.listFeedback(limit = 20)
    result.onSuccess { page ->
        page.posts.forEach { post ->
            println("${post.title} — ${post.upvotes} upvotes")
        }
    }
}

Pass boardId to filter by board, or cursor for pagination.

FeedbackJar.getPost(postId) fetches a single public post the same way.

Voting

Guest upvoting as this install’s anonymous guest — idempotent, and gated by the project’s Allow guest votes setting (WidgetConfig.allowVotes, see Get widget config):

kotlin
lifecycleScope.launch {
    FeedbackJar.vote(postId)          // upvote
    FeedbackJar.unvote(postId)        // remove the upvote
    val state = FeedbackJar.getVoteState(postId) // current count + hasVoted
}

Comments

Anonymous, two-level threads — gated by Allow guest comments (WidgetConfig.allowComments):

kotlin
lifecycleScope.launch {
    val comments = FeedbackJar.listComments(postId, limit = 20)

    // Reply to a post, or to a root comment via parentId (replies-to-replies aren't allowed)
    FeedbackJar.addComment(postId, "Same issue here!", parentId = null)
}

name/email fall back to the remembered identity (setIdentity) when omitted.

Get widget config

Check whether the org asks for name/email, and whether voting/comments are enabled, before building your own UI around them:

kotlin
val config = FeedbackJar.getConfig().getOrNull()
if (config?.collectEmail == true) { /* show an email field */ }
if (config?.allowVotes == true) { /* show your vote button */ }
FieldMeaning
collectNameOrg asks submitters for their name (“Ask for Name”)
collectEmailOrg asks submitters for their email (“Ask for Email”)
allowVotesGuest upvoting is enabled for this project
allowCommentsGuest commenting is enabled for this project

API reference

MethodDescription
init(context, widgetId)Initialize the SDK. Call once before anything else.
setIdentity(name?, email?)Remember a submitter’s name/email for later calls.
setIdentity(userId, email, signature, timestamp, ...)Set a verified, org-signed identity — see Verified identity.
getIdentity()The currently remembered identity, if any.
clearIdentity()Forget the remembered identity (e.g. on logout).
suspend submit(content, email?, userName?, properties?)Submit feedback. Returns Result<FeedbackResponse>.
suspend listFeedback(boardId?, limit?, cursor?)List public feedback. limit is clamped to 1–50.
suspend getPost(postId)Fetch a single public post.
suspend getConfig()Fetch this project’s WidgetConfig.
suspend vote(postId) / unvote(postId)Cast or remove this install’s upvote. Idempotent.
suspend getVoteState(postId)Current upvote count and whether this install voted.
suspend listComments(postId, limit?, cursor?)List a post’s comments (two levels).
suspend addComment(postId, content, parentId?, name?, email?)Add a comment or reply.

All suspend methods have a callback overload (callback: (Result<T>) -> Unit) safe to call from the main thread.

Notes

  • Submission, voting, and comments are all anonymous by default — setIdentity attaches a name/email, it does not require a full account or portal login.
  • Voting and commenting only work when the project has them enabled — check getConfig() before showing that UI.
  • The server rate-limits submissions (5 per 15 minutes per IP).
  • Private boards and non-public posts are never returned by listFeedback.