Documentation

$folder

A directory the user picked, remembered across restarts, and the files in it.

val library = $folder("library")

@if (library.granted) {
  <Audio src={library.file("book/03.mp3")} background />
  <Document src={library.page("book/03.html")} />
} else {
  <Button onClick={() => library.choose()}>Choose your library</Button>
}

Why this exists

static/ is what the developer shipped. A folder is what the user pointed at, and its contents are unknown at build time: a downloaded book, a music library, an export. An app needs somewhere to put those files and a way to hand them to <Audio>, <Image>, <Video> and <Document>.

No permission is involved

The user grants one directory through the system folder picker, not the storage volume. No <uses-permission> entry is added and nothing is asked with $permission. The grant survives a restart. The key, "library" here, names which grant; an app may hold several, each remembered separately. The set of folders an app has is the set of $folder(…) calls in its source.

Members

  • granted — a folder is chosen and the grant still holds (state)
  • name — the chosen folder's display name (state)
  • choose() — open the system folder picker; does nothing with no activity on screen
  • file(path) — a content:// address for any src that takes one. It does not check the file exists
  • page(path) — an address <Document> can resolve siblings against, so a page's stylesheet and images load from the folder
  • list(path) — the names directly inside a path, sorted; with no argument, inside the folder itself
  • exists(path) — whether a path is there; asks the provider
  • forget() — give the grant back; the files are untouched
  • refresh() — re-read the grant; done for you when the app resumes
  • download(url, to = path) — fetch into the folder, and watch it arrive

file and page are different on purpose. A content:// document cannot resolve the stylesheet beside it, so a <Document> shown from file() would arrive as unstyled text. page() serves the folder so relative links work.

download

val job = library.download("https://example.com/book.zip", to = "book/book.zip")

job.progress   // 0..1, or -1 when the server did not say how big the file is
job.bytes      // written so far
job.done
job.failed     // the failure as a message, null while it is going fine
job.cancel()   // what has already been written stays written

The handle is a live object rather than a call you wait on, because a download can outlive the screen that asked for it. Calling download again with the same destination returns the same handle, so reading it in markup starts one download, not one per redraw. Missing directories in to are created.

Everything here is empty until a folder is chosen: file() returns an empty string and list() an empty list. A screen can be written for the granted case and shown behind library.granted.

Try it

Library.wh
import $lib.components.ui.Button

val library = $folder("library")

<Column class="gap-8">
  @if (library.granted) {
    <Text>{library.name}</Text>
    <Button onClick={() => library.forget()}>Forget</Button>
  } else {
    <Button onClick={() => library.choose()}>Choose your library</Button>
  }
</Column>
Kotlin waiting
Scroll into view to compile.

granted is state, so the screen changes by itself once a folder is chosen. The key names which grant; it is not a path.

See Also