Skip to main content

Getting Started

Installation​

TODO publish to maven

Quick Start​

For RESTful APIs with JSON data, first you need to configure HTTP client and JSON parsing, to use it in a fetcher function. You can use Ktor client with kotlinx.serialization serializer:

val client = HttpClient { install(JsonFeature) }

@Serializable
data class IpResponse(val ip: String)

Then you can and start using useSWR inside any @Composable function:

@Composable
fun App() {
val (data, error) = useSWR(
key = "https://api.ipify.org?format=json",
fetcher = { client.request<IpResponse>(it) }
)

when {
error != null -> Text(text = "Failed to load")
data != null -> Text(text = data.ip)
else -> Text(text = "Loading")
}
}

Normally, there're 3 possible states of a request: "loading", "ready", or "error". You can use the value of data and error to determine the current state of the request, and return the corresponding UI.

Make It Reusable​

When building an app, you might need to reuse the data in many places of the UI. It is incredibly easy to create reusable data hooks on top of SWR:

sealed class IpUiState {
object Loading : IpUiState()
object Error : IpUiState()
data class Loaded(val ip: String) : IpUiState()
}

@Composable
fun useIp(): IpUiState {
val (data, error) = useSWR(
key = "https://api.ipify.org?format=json",
fetcher = { client.request<IpResponse>(it) }
)

return when {
error != null -> IpUiState.Error
data != null -> IpUiState.Loaded(data.ip)
else -> IpUiState.Loading
}
}

And use it in your components:

@Composable
fun IpComponent() {
when (val ipState = useIp()) {
is IpUiState.Loading -> CircularProgressIndicator()
is IpUiState.Error -> ErrorBox()
is IpUiState.Loaded -> IpBox(ip = ipState.ip)
}
}

By adopting this pattern, you can forget about fetching data in the imperative way: start the request, update the loading state, and return the final result. Instead, your code is more declarative: you just need to specify what data is used by the component.

Fetcher abstraction and scoping​

Storing bare HttpClient as the global property is not considered to be a good practice and may lead to memory leaks. It is suggested to use abstraction to hide http implementation details from composables, e.g:

internal sealed class Request<out D>(val url: String) {
object Ip : Request<IpResponse>(url = "https://api.ipify.org?format=json")
}

internal class Fetcher(internal val client: HttpClient) {

internal suspend inline operator fun <reified T> invoke(request: Request<T>) : T {
return client.request(request.url)
}
}

Then, you should scope Fetcher instance to Composable application. It can be done using cokoin - Compose DI library:

val apiModule = module {
single { HttpClient { install(JsonFeature) } }

single { Fetcher(client = get()) }
}

@Composable
fun useIp(): IpUiState {
val fetcher = get<Fetcher>()
val (data, error) = useSWR(
key = Request.Ip,
fetcher = { fetcher(it) },
)
return when {
error != null -> IpUiState.Error
data != null -> IpUiState.Loaded(data.ip)
else -> IpUiState.Loading
}
}

@Composable
fun App() {
Koin(appDeclaration = { modules(apiModule) }) {
IpComponent()
}
}