Thomas Pedot
mardi 28 avril 2026
Background Tasks in Tauri Android: WorkManager + Native Notifications

GitAlchemy needs to notify users when new todos appear — even when the app is closed. On Android, this requires WorkManager: Android's recommended API for deferrable, guaranteed background work. This article covers the full implementation chain: Kotlin worker → Rust IPC → React frontend → native system notification.
Why WorkManager
Android killing background processes is not a bug — it's a feature. Apps that keep CPU awake to poll for updates get terminated by the system. WorkManager solves this by:
- Guaranteed execution — persists work across device reboots, battery optimization, and app updates
- Platform-aware scheduling — uses JobScheduler on Android 6+, AlarmManager on older versions, or Firebase Cloud Messaging when available
- Battery compliance — batches work intelligently to minimize battery drain
For a GitLab client that needs to check for new todos every 15-30 minutes, WorkManager is the only reliable solution on Android.
The Implementation Chain
The flow is: AndroidManifest.xml registers the worker → Kotlin worker class runs on schedule → calls Rust Tauri command → Rust fetches GitLab todos → compares with last-seen IDs → sends native notification via tauri-plugin-notification.
AndroidManifest.xml
The worker must be declared with a name and exported so the system can instantiate it:
1<provider
2 android:name="androidx.startup.InitializationProvider"
3 android:authorities="${applicationId}.androidx-startup"
4 android:exported="false"
5 tools:node="merge">
6 <meta-data
7 android:name="androidx.work.WorkManagerInitializer"
8 android:value="androidx.startup"
9 tools:node="remove" />
10</provider>The tools:node="remove" disables WorkManager's default lazy initialization, allowing us to use the explicit Configuration we set in the Application class.
Kotlin Worker Class
The worker runs in a background thread (not the main thread), so network calls are safe. It receives the app context and the Tauri app handle:
1class TodoSyncWorker(
2 context: Context,
3 params: WorkerParameters,
4 private val appHandle: AppHandle
5) : CoroutineWorker(context, params) {
6
7 override suspend fun doWork(): Result {
8 return try {
9 val command = "plugin:todos|get_todos"
10 val response = appHandle.invoke(command, null)
11
12 val todos = (response as? List<*>)?.mapNotNull { obj ->
13 (obj as? Map<*, *>)?.let {
14 Todo(
15 id = it["id"] as? String ?: return@mapNotNull null,
16 title = it["title"] as? String ?: ""
17 )
18 }
19 } ?: return Result.failure()
20
21 val lastSeenFile = File(applicationContext.filesDir, "last_seen_todos.json")
22 val lastSeenIds = if (lastSeenFile.exists()) {
23 Gson().fromJson(lastSeenFile.readText(), Array<String>::class.java).toSet()
24 } else emptySet()
25
26 val newTodos = todos.filter { it.id !in lastSeenIds }
27
28 if (newTodos.isNotEmpty()) {
29 val notification = appHandle.notification()
30 .builder()
31 .title("${newTodos.size} new todo(s)")
32 .body(newTodos.first().title)
33 .build()
34 notification.show()
35
36 lastSeenFile.writeText(Gson().toJson(todos.map { it.id }.toTypedArray()))
37 }
38
39 Result.success()
40 } catch (e: Exception) {
41 Result.failure()
42 }
43 }
44}The worker uses Tauri's invoke system to call the Rust get_todos command, then compares with previously-seen IDs stored in a JSON file. When new todos exist, it builds and shows a native notification, then updates the file.
Wiring WorkManager in MainActivity
The Application class configures WorkManager and enqueues the periodic work:
1class MainActivity : TauriActivity() {
2 override fun onCreate(savedInstanceState: Bundle?) {
3 super.onCreate(savedInstanceState)
4
5 val config = Configuration.Builder()
6 .setMinimumLoggingLevel(Log.DEBUG)
7 .build()
8
9 WorkManager.initialize(this, config)
10
11 val constraints = Constraints.Builder()
12 .setRequiredNetworkType(NetworkType.CONNECTED)
13 .build()
14
15 val workRequest = PeriodicWorkRequestBuilder<TodoSyncWorker>(
16 15, TimeUnit.MINUTES,
17 5, TimeUnit.MINUTES // flex interval
18 )
19 .setConstraints(constraints)
20 .addTag("todo-sync")
21 .build()
22
23 WorkManager.getInstance(this).enqueueUniquePeriodicWork(
24 "todo-sync-work",
25 ExistingPeriodicWorkPolicy.KEEP,
26 workRequest
27 )
28 }
29}The periodic work runs every 15 minutes with a 5-minute flex window — Android decides the optimal time based on battery state and other system work.
Rust Command Layer
The Rust side provides the get_todos command that the worker calls:
#[tauri::command]
pub async fn get_todos(app: AppHandle) -> Result<Vec<Todo>, String> {
// Load cached GitLab token, fetch todos from GitLab API
// Returns Vec<Todo> serializable to JSON
}This command lives in the same Rust codebase as the desktop app — the worker just calls it via Tauri IPC like any other frontend call.
React Frontend Integration
The React side manages the background session state and can manually trigger notification resets for testing:
1import { invoke } from "@tauri-apps/api/core";
2
3export async function resetSeenNotifications() {
4 const result = await invoke<{ status: string }>("clear_seen_notifications");
5 if (result.status === "ok") {
6 toast.success("Notification cache reset");
7 }
8}A command palette command (notification.reset-seen) lets developers test that notifications fire correctly without waiting for the next background cycle.
Key Takeaways
- WorkManager is the Android-standard way to run reliable background tasks — don't fight the system
- The worker calls Rust commands via Tauri invoke, keeping the logic in the shared codebase
- Native notifications require tauri-plugin-notification, not the web-based toast system
- Test on real devices with battery optimization enabled — emulators often allow more aggressive background execution
This builds on the F-Droid distribution setup covered in the previous article. The same codebase now runs on desktop, Android, and iOS — with background sync on Android.
Related Articles
- GitAlchemy v1.3 Release Notes — Overview of all new features
- Building a Rich Text Editor with TipTap on Mobile — Rich text editing in the same mobile app
- Rust + Vite + GitLab Push Events — The full stack integration
- Tauri React TypeScript Production App — Core architecture
- GitAlchemy Raycast Design System — UI overhaul that enabled this editor