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:
starts with a capital case letter (PascalCase)
be a noun which MAY be prefixed by descriptive adjectives
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:
Memory: ability to call
remember
functions.Lifecycle: effects launched within its body will live as long as the composable lives. For instance, we can have a job spanning across recompositions.
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.
Idempotent: the function behaves the same way when called with the same arguments.
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 CompositionLocal
s 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 remember
ed 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.