Share data between Android apps and phones without a server
A small app rarely needs its own backend. Android already connects every app on the phone and every phone nearby. With the share sheet, "Open with…", the Storage Access Framework and local Wi-Fi, your app can send and receive data through tools people already use, with no accounts, no cloud bill and no server to maintain.
Key takeaways
Intent.ACTION_SENDplusIntent.createChooserreaches WhatsApp, email, Bluetooth, Quick Share and every other installed app.- Intent filters for
SENDandVIEWlet your app receive files from anywhere on the phone. CreateDocumentandOpenDocumentsave and load files in Downloads, Documents or cloud folders without storage permissions.- A plain CSV with a header row keeps the data readable in Excel and Google Sheets.
Why a no-backend app is often the better choice
A server brings accounts, passwords, hosting costs, privacy obligations and something that can go down. For a team tool, a personal tracker or a prototype, that cost often outweighs the benefit. Meanwhile, every Android phone already has a dozen ways to move a file: messaging apps, email, Bluetooth, Nearby/Quick Share, and whatever cloud drives the user has connected.
Designing around these channels also respects how people already work. A supervisor who lives in a WhatsApp group would rather receive a CSV there than install another app and create another login.
Send with the Android share sheet (ACTION_SEND)
The system share sheet is the most powerful feature most small apps ignore. Build an Intent.ACTION_SEND, set the MIME type and content, and wrap it in a chooser:
fun shareCsv(context: Context, csvText: String) {
val sendIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/csv"
putExtra(Intent.EXTRA_TEXT, csvText)
putExtra(Intent.EXTRA_SUBJECT, "Inventory export")
}
context.startActivity(Intent.createChooser(sendIntent, "Share inventory CSV"))
}
The user picks the destination. Your app doesn't need to know or care whether it went to email, a chat, Google Drive or a laptop over Quick Share.
Sharing text in EXTRA_TEXT is the simplest option and works well for small data. For large files, or when the receiver should see an attachment, write the file to your cache directory and share a content:// URI from a FileProvider with EXTRA_STREAM and FLAG_GRANT_READ_URI_PERMISSION.
Receive files with "Open with…" and "Share to"
Receiving is just as easy. Declare intent filters on your Activity and Android will list your app when the user shares text or taps a CSV in a file manager or email attachment. The manifest that android_scaffold generates already registers these on the launcher Activity:
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/csv" />
</intent-filter>
Then read the incoming intent in onCreate and in onNewIntent, because the app may already be running:
private fun receiveCsvFrom(intent: Intent?) {
val sharedCsv = when (intent?.action) {
Intent.ACTION_SEND -> intent.getStringExtra(Intent.EXTRA_TEXT)
Intent.ACTION_VIEW -> intent.data?.let { readTextDocument(contentResolver, it) }
else -> null
}
if (sharedCsv != null) incomingSharedCsv = sharedCsv
}
Always show what arrived and let the user decide what to do with it before changing their data. See merge or replace below.
Save and load files with the Storage Access Framework
The Storage Access Framework (SAF) shows the system file picker. The user chooses where to save or what to open, including Downloads, Documents, an SD card or any cloud drive app that plugs into the picker, such as Google Drive. Your app needs no storage permission, because the user grants access to one file at a time.
val saveLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("text/csv")
) { uri ->
uri?.let { context.contentResolver.openOutputStream(it)?.use { stream -> stream.write(csvText.toByteArray()) } }
}
val openLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let { importCsv(context.contentResolver.openInputStream(it)?.bufferedReader()?.readText()) }
}
saveLauncher.launch("inventory.csv")
openLauncher.launch(arrayOf("text/csv", "text/comma-separated-values", "text/plain"))
Saving a file to a shared cloud folder is a surprisingly effective "sync" for a small team: everyone saves into the same folder, and the latest file is always there.
Phone-to-phone transfer over local Wi-Fi
When two people are standing next to each other, the fastest path is often direct. Two phones on the same Wi-Fi network, or one phone running a hotspot that the other joins, can talk over ordinary sockets without any internet connection.
The Thumb Inventory example does this in a single 133-line file using plain Java sockets and coroutines:
- The sender opens a TCP server socket on a fixed port (48921) and broadcasts a small UDP beacon containing its IP address.
- The receiver listens for the beacon to discover the sender automatically.
- The receiver connects over TCP and downloads the CSV, then the sender closes the socket.
- If the network blocks broadcasts, as some guest and corporate Wi-Fi does, the receiver can type the sender's IP address instead.
Network calls must run off the main thread (the example uses withContext(Dispatchers.IO)), and the app needs the INTERNET permission even for local traffic. For richer peer-to-peer features, Android also offers Wi-Fi Direct and the Nearby Connections API in Google Play services.
Use CSV as the interchange format
Whatever channel you use, the payload matters more. CSV is not glamorous, but it opens everywhere: Excel, Google Sheets, LibreOffice, a text editor, a database import, a Python script. That makes your app's data useful far beyond your app.
- Write a header row with stable, simple column names.
- Quote fields that contain commas, quotes or line breaks.
- Use UTF-8 so names and symbols survive the round trip.
- Read columns by header name where you can, and tolerate missing optional columns.
- Accept
text/plainandtext/comma-separated-valuesas well astext/csv; apps label CSV files inconsistently.
Merge or replace: let the user decide
When data arrives from somewhere else, don't silently overwrite what's on the phone. Show a short summary ("Load 24 items from a shared file. You currently have 31 items.") and offer clear choices:
Merge
Combine the incoming records with the current ones. Thumb Inventory matches items by name and adds quantities, which suits counts from several people.
Replace
Clear the current data and use only the incoming file. Best when one device holds the master list. Offer Undo straight after.
Read more about conflicts and syncing in Offline-first field apps.
Frequently asked questions
How do I share a file from my Android app to other apps?
Create an Intent with Intent.ACTION_SEND, set its type and put the data in EXTRA_TEXT or a content URI in EXTRA_STREAM, then start it with Intent.createChooser. The user picks the destination app from the system share sheet.
How do I make my app appear in "Open with" for CSV files?
Add an intent filter to your Activity for the android.intent.action.VIEW action with the DEFAULT category and the text/csv MIME type, then read intent.data with the content resolver in onCreate and onNewIntent.
Do I need storage permissions to save files to Downloads?
No. With the Storage Access Framework, ActivityResultContracts.CreateDocument and OpenDocument show the system file picker and grant your app access to the single file the user chooses, without any storage permission.
Can two Android phones exchange data without internet?
Yes. Phones on the same Wi-Fi network or hotspot can connect with ordinary TCP sockets, as in the Thumb Inventory example. Bluetooth and Quick Share through the share sheet also work offline.