Composable functions and return types

May 21, 2022

I recently came across the official API Guidelines for Jetpack Compose and thought it was worth sharing along with some extra examples. Let’s look at composable return types in this post. In follow-up posts, we can look at other Compose API topics.

TL;DR: Composables can emit UI or return results, but not both.

Composable return types

Unit return type

In most cases Composable functions will return a Unit. Such composables emit UI. For instance:

@Composable
fun ContactList(
  contacts: List<Contact>,
  modifier: Modifier = Modifier
) {

The naming convention is:

  1. starts with a capital case letter (PascalCase)

  2. be a noun which MAY be prefixed by descriptive adjectives

  3. convention applies whether Composable emits UI elements or not.

In the example above, Compose compiler will map the data passed in into a ContactList UI node which will then be emitted into the overall UI tree. Composable functions that build UI don’t return anything (they return Unit) because they construct a representation of the UI state instead of constructing and returning a UI widget.

By making this function Composable, we get access to:

  1. Memory: ability to call remember functions.

  2. Lifecycle: effects launched within its body will live as long as the composable lives. For instance, we can have a job spanning across recompositions.

  3. Access to composition tree: this composable will be a part of the overall composition tree and have access to CompositionLocal

Check out https://jorgecastillo.dev/book/ for an in-depth look at Compose internals.

Because Composable functions can be recomposed any time, it is our responsibility to make Composable functions that we write idempotent and free of side effects.

  1. Idempotent: the function behaves the same way when called with the same arguments.

  2. Free of side effects: the function does not affect global state.

Factory function

Alternatively, a Composable function may return a value, i.e. may serve as a factory. Such composables do not emit UI. Normally, you will call them from a Composable that emits UI.

By convention, the name of Composable factory functions should start with a lower case letter.

Making this factory function a @Composable lets it use composition lifecycle and/or using CompositionLocals as inputs to construct itself.

Here are some example signatures:

// Returns a style based on the current CompositionLocal settings
@Composable
fun defaultStyle(): Style {

And here is a library function which remembers a value prior to returning it (notice the prefix in the function name):

@Composable
fun rememberCoroutineScope(): CoroutineScope {

It returns CoroutineScope and, as indicated by the function name, the coroutine scope is remembered prior to being returned. This prefix is a convention.

In this case, the remember suggests automatic cancellation behavior—the coroutine scope will be cancelled when this call leaves the composition from which a Job in this CoroutineScope is launched.

Composition implies initial composition and 0 or more recompositions:

Here is a full source code for that function (notice the remember):

@Composable
inline fun rememberCoroutineScope(
    getContext: @DisallowComposableCalls () -> CoroutineContext = { EmptyCoroutineContext }
): CoroutineScope {
    val composer = currentComposer
    val wrapper = remember {
        CompositionScopedCoroutineScopeCanceller(
            createCompositionCoroutineScope(getContext(), composer)
        )
    }
    return wrapper.coroutineScope
}

Not every Composable function that returns a value is a factory function. To be considered a factory, this must be a primary purpose of the composable function.

To summarize, if you write your own Composable factory function (function that returns a value), you usually will 1) get access to or generate a value/state, 2) optionally remember it and finally 3) return it.

Here are some examples from JetSnack Google sample:

@Composable
private fun rememberSearchState(
    query: TextFieldValue = TextFieldValue(""),
    focused: Boolean = false,
    searching: Boolean = false,
    categories: List<SearchCategoryCollection> = SearchRepo.getCategories(),
    suggestions: List<SearchSuggestionGroup> = SearchRepo.getSuggestions(),
    filters: List<Filter> = SnackRepo.getFilters(),
    searchResults: List<Snack> = emptyList()
): SearchState {
    return remember {
        SearchState(
            query = query,
            focused = focused,
            searching = searching,
            categories = categories,
            suggestions = suggestions,
            filters = filters,
            searchResults = searchResults
        )
    }
}
@Composable
fun rememberJetcasterAppState(
    navController: NavHostController = rememberNavController(),
    context: Context = LocalContext.current
) = remember(navController, context) {
    JetcasterAppState(navController, context)
}

And here is an example from ComposeCookBook which creates and remembers a MapView and gives it the lifecycle of the current LifecycleOwner:

@Composable
fun rememberMapViewWithLifecycle(): MapView {
    val context = LocalContext.current
    val mapView = remember {
        MapView(context)
    }

    // Makes MapView follow the lifecycle of this composable
    val lifecycleObserver = rememberMapLifecycleObserver(mapView)
    val lifecycle = LocalLifecycleOwner.current.lifecycle
    DisposableEffect(lifecycle) {
        lifecycle.addObserver(lifecycleObserver)
        onDispose {
            lifecycle.removeObserver(lifecycleObserver)
        }
    }

    return mapView
}

@Composable
private fun rememberMapLifecycleObserver(mapView: MapView): LifecycleEventObserver =
    remember(mapView) {
        LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle())
                Lifecycle.Event.ON_START -> mapView.onStart()
                Lifecycle.Event.ON_RESUME -> mapView.onResume()
                Lifecycle.Event.ON_PAUSE -> mapView.onPause()
                Lifecycle.Event.ON_STOP -> mapView.onStop()
                Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
                else -> throw IllegalStateException()
            }
        }
    }

Note that while the MapView example above still serves as a good example of a composable function that returns non-Unit type, Maps Compose library is now available which may make custom code like above unnecessary.

Previous
Previous

Lazy layouts contentPadding

Next
Next

Compose phases and optimizations