Thomas Pedot

mardi 28 avril 2026

Background Tasks in Tauri Android: WorkManager + Native Notifications

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:

XML
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:

KOTLIN
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:

KOTLIN
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:

RUST
#[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:

TYPESCRIPT
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