I changed a file-opening routine from local paths to URLs and immediately made the window feel stuck. The transfer was not especially slow. My code was waiting synchronously in the user-interface path, so even a short delay became visible.

KIO is built around jobs. A call such as KIO::get() starts work and returns a KIO::TransferJob; data and completion arrive through signals while the event loop continues processing input and painting.

KIO::TransferJob *job = KIO::get(url, false, false);
connect(job, SIGNAL(data(KIO::Job *, const QByteArray &)),
        this, SLOT(slotData(KIO::Job *, const QByteArray &)));
connect(job, SIGNAL(result(KIO::Job *)),
        this, SLOT(slotResult(KIO::Job *)));

The job chooses an appropriate KIO slave from the URL’s protocol. The slave runs separately, speaks the protocol, and reports data, progress, redirections, and errors back through KIO. My application consumes a common job interface instead of containing FTP, HTTP, and file code.

There are two details I now handle every time. First, data may arrive in several chunks. The data signal is not a promise that one byte array equals one document. I append or parse incrementally. Second, I treat result as the final authority. An empty final chunk does not mean success, and a non-empty first chunk does not mean the transfer will finish.

For a tiny operation, KIO::NetAccess can provide a convenient synchronous wrapper. I still avoid it from slots reached directly through buttons and menus. Nested event loops and blocked windows are an expensive price for saving two slots.

Cancellation also matters. I keep the job pointer, clear it when the job finishes, and kill the job when the owning view goes away. Because jobs are QObjects, sensible parentage helps, but explicit user cancellation should still be reflected in the interface.

Redirection is another reason not to reduce a transfer to one blocking read. The final URL may differ from the requested one, and policy about accepting it belongs in the job flow. Authentication and certificate questions can also require interaction supplied by KDE. A hand-written socket loop tends to rediscover these cases individually, generally when somebody is trying to use the program rather than when I am prepared to debug it.

For uploads I apply the same rule in reverse: feed data when the job requests it and finish according to the job’s protocol. I do not assume that writing a local temporary file and copying it afterward has identical overwrite and error behavior.

I prefer KIO jobs even for local URLs when the operation already accepts a KURL. The mechanism remains uniform and remote files stop being an awkward special case. The caveat is that asynchronous code forces the state to be honest. That is inconvenient for about an hour and useful for the rest of the program’s life.