Kotlin Multiplatform

Feature requests for Kotlin Multiplatform apps: one board, every target

Kotlin Multiplatform lets you share business logic between Android and iOS, and Compose Multiplatform now shares the UI too. Feedback tooling has not caught up: almost no feature request product ships a KMP library, and the ones with native Android SDKs leave the iOS target with nothing. This page shows how to add feature requests to a Kotlin Multiplatform app with a hosted board and a two-line expect/actual, plus a Ktor option if you want to render the board in Compose yourself.

For Kotlin Multiplatform, declare an expect fun that opens your hosted feature request board, implement it with Chrome Custom Tabs in androidMain and SFSafariViewController in iosMain, and call it from shared code. No third-party KMP library is needed, both targets share one board, and the dashboard shows the Android-versus-iOS split. Free for 10 requests, then $5 a month.

Why feedback tools are a gap in the KMP ecosystem

Kotlin Multiplatform projects pick dependencies carefully. Every library has to either be multiplatform itself or be wrapped behind expect/actual, and every wrapper is code you maintain through Kotlin, Compose, and Xcode upgrades. Feedback and feature request tools are almost all web products with a JavaScript widget; the few with mobile SDKs offer Android and Swift packages separately, which means two integrations and two sets of release notes to track.

There is a second cost that is easy to miss. If Android uses one tool and iOS uses another, or the same tool with two projects, votes are split. A request with 60 votes on Android and 45 on iOS shows up as two mid-sized asks instead of your most requested feature. For a team that adopted KMP precisely to stop doing things twice, that is the wrong outcome.

Pricing follows the same pattern as the rest of the category. Canny Pro is $79 a month and bills per tracked user. Featurebase Basic is $59 a month. Nolt is $29 a month. pick a feature is free for one project and 10 requests, then $5 a month flat, and the integration below has no library to maintain at all.

Open the hosted board from shared code

Your project's board lives at a URL you copy from the dashboard, for example https://pickafeature.com/b/your-app. Declare one function in commonMain and implement it per target.

commonMain/FeatureRequests.kt
expect fun openFeatureRequests(url: String)

const val FEATURE_REQUESTS_URL = "https://pickafeature.com/b/your-app"
androidMain/FeatureRequests.android.kt
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent

// Needs implementation("androidx.browser:browser:1.8.0") in androidMain
// and an application/activity Context provider of your choice.
actual fun openFeatureRequests(url: String) {
    val context = AppContext.get()
    CustomTabsIntent.Builder()
        .setShowTitle(true)
        .build()
        .launchUrl(context, Uri.parse(url))
}
iosMain/FeatureRequests.ios.kt
import platform.Foundation.NSURL
import platform.SafariServices.SFSafariViewController
import platform.UIKit.UIApplication

actual fun openFeatureRequests(url: String) {
    val nsUrl = NSURL(string = url)
    val controller = SFSafariViewController(uRL = nsUrl)
    val root = UIApplication.sharedApplication.keyWindow?.rootViewController
    root?.presentViewController(controller, animated = true, completion = null)
}

Then, in Compose Multiplatform shared UI:

commonMain/SettingsScreen.kt
@Composable
fun SettingsScreen() {
    ListItem(
        headlineContent = { Text("Feature requests") },
        supportingContent = { Text("Tell us what to build next") },
        modifier = Modifier.clickable { openFeatureRequests(FEATURE_REQUESTS_URL) }
    )
}

That is the entire integration. The board follows the system theme on both platforms, stores votes in the browser's storage so each person votes once per request, and shows only requests you have moved to planned, in progress, or completed.

Rendering the board in Compose with the REST API

If you want the list inside your own Compose screens rather than a browser sheet, the backend is four JSON endpoints and works from any HTTP client. With Ktor in commonMain:

commonMain/FeedbackApi.kt
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.serialization.Serializable

@Serializable data class FeatureRequest(
    val id: String, val title: String, val description: String,
    val status: String, val upvotes: Int
)
@Serializable private data class ListResponse(val featureRequests: List<FeatureRequest>)

class FeedbackApi(private val client: HttpClient, private val apiKey: String) {
    private val base = "https://pickafeature.com/api/v1/sdk"

    suspend fun list(): List<FeatureRequest> =
        client.get("$base/feedback") { header("x-api-key", apiKey) }
            .body<ListResponse>().featureRequests

    suspend fun vote(id: String, deviceId: String) =
        client.post("$base/vote") {
            header("x-api-key", apiKey)
            contentType(ContentType.Application.Json)
            setBody(mapOf("featureRequestId" to id, "deviceId" to deviceId,
                          "direction" to "up", "platform" to platformName()))
        }
}

Generate a device id once and keep it in multiplatform settings; pass an optional userId if your app has accounts so votes follow the user. Sending platform as "android" or "ios" is what powers the per-platform charts in the dashboard. The full request and response shapes are in the REST API docs, and the open source Flutter SDK is a complete reference client if you want to port the UI.

One dashboard for both targets

Whichever integration you pick, both targets write to one project. Requests arrive as pending and stay hidden until you approve them, which keeps spam off the public board. A kanban board handles triage: drag to under review, planned, in progress, and completed, and users see the change on their next visit.

Duplicates across platforms are common because iOS users and Android users describe the same feature differently. Drag one card onto another and the merge dialog lets you combine titles and descriptions and carry votes over with each voter counted once. The analytics page splits users and requests by Android, iOS, and web, so a "share sheet" request can be recognised as an iOS ask before anyone builds it for Android.

How pick a feature fits a Kotlin Multiplatform team

pick a feature is a flat-priced feedback board for developers who pay their own bills: free for one project and 10 feature requests, $5 a month for three projects with unlimited requests, $15 a month for unlimited projects. No per-seat charges and no per-tracked-user tiers.

For KMP that means a hosted board that works from shared code with two small actuals, a REST API you can call from Ktor, and a dashboard with merge and per-platform analytics. A published Kotlin Multiplatform library is on the roadmap and prioritised by demand; tell us if that is what would make you switch.

Frequently asked questions

Is there a Kotlin Multiplatform library for pick a feature?

Not yet. The supported routes are the hosted board, opened through a two-line expect/actual, and the REST API called from Ktor or any HTTP client. Both are documented on this page and write to the same project. A published KMP library is on the roadmap and prioritised by demand.

Does this work with Compose Multiplatform and iOS?

Yes. The Android actual uses Chrome Custom Tabs and the iOS actual presents SFSafariViewController from the root view controller. Both are called from a shared Composable. On desktop or web targets, open the board URL with the platform's default browser API.

How do votes stay one per user?

On the hosted board, a stable id is stored in the browser and the server accepts one vote per request per id. With the REST API, you send your own device id or user id, and the server enforces the same rule against that id.

Can I see which platform a request came from?

Yes. The hosted board and the web widget report the platform automatically, and the REST API accepts a platform field. The analytics page shows users and requests broken down by Android, iOS, and web.

What does it cost?

Free for one project and 10 feature requests with no card required. Starter is $5 a month for three projects and unlimited requests. Unlimited is $15 a month for unlimited projects. Pricing is flat and independent of install counts or team size.

Start collecting feature requests today

Free plan: 1 project, 10 feature requests, no credit card. Paid plans from $5 a month.