Hello, this is Android Engineer Shakil from Product Engineering Department.
Jetpack Compose is a modern, fully declarative UI toolkit for building native Android user interfaces. In this article I would like to talk about five common pitfalls that one may fall into while writing compose code.
1. Calling non-compose code in composable functions
Bad Code
@Composable private fun ReadingList( ) { val scope = rememberCoroutineScope() val books by remember { mutableStateOf<List<Book>>(emptyList()) } // THIS IS BAD // This will launch a new coroutine with a long running network // call to load books, whenever this ReadingList composable is recomposed. // This can lead to performance issues or unexpected behavior. scope.launch { books = loadReadingList() } LazyColumn { items(books) { // ... } } }
Good Code
@Composable private fun ReadingList( ) { var books by remember { mutableStateOf<List<Book>>(emptyList()) } // THIS IS GOOD // Using effect handlers of Jetpack Compose will ensure // that code within is not executed everytime the composable // is recomposed. LaunchedEffect(Unit) { books = loadReadingList() } LazyColumn { items(books) { // ... } } }
2. Not using keys inside a lazy column
Bad Code
@Composable private fun ReadingList(books: List<Book>) { // THIS IS BAD // Every visible composable in the LazyColumn will be recomposed // whenever there are any changes in books. LazyColumn { items(books) { book -> // ... } } }
Good Code
@Composable private fun ReadingList(books: List<Book>) { // THIS IS GOOD // By utilizing the keys lambda of LazyColumn, // each item can be uniquely identified. // So, only items that have actually changed will be recomposed. LazyColumn { items( items = books, key = { book -> book.id } ) { book -> // ... } } }
3. Consuming flows with collectAsState()
Bad Code
class YakitoriViewModel: ViewModel() { private val _yakitoriEaten = MutableStateFlow(0) val yakitoriEaten = _yakitoriEaten.onEach { saveCounterToLocalCache(it) }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(7000), 0 ) } @Composable private fun YakitoriEaten( viewModel: YakitoriViewModel = hiltViewModel(), ) { // THIS IS BAD // The Flow will be executed and will // run DB operations even if the app is on the background. val yakitoriEaten = viewModel.yakitoriEaten.collectAsState() Text(text = yakitoriEaten.toString()) }
Good Code
class YakitoriViewModel: ViewModel() { private val _yakitoriEaten = MutableStateFlow(0) val yakitoriEaten = _yakitoriEaten.onEach { saveCounterToLocalCache(it) }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(7000), 0 ) } @Composable private fun YakitoriEaten( viewModel: YakitoriViewModel = hiltViewModel(), ) { // THIS IS GOOD // Using collectAsStateWithLifecycle() ensures that // the Flow collector is lifecycle-aware and will // not run if the app goes in the background. val yakitoriEaten = viewModel.yakitoriEaten.collectAsStateWithLifecycle() Text(text = yakitoriEaten.toString()) }
4. Not using remember for heavy computations
Bad Code
@Composable private fun EncryptedShibaInu( encryptedBytes: ByteArray ) { // THIS IS BAD // Here, the bytes will be decrypted everytime a recomposition occurs // which is a lot of computation. val decryptedBytes = CryptoManager.decrypt(encryptedBytes) val bitmap = BitmapFactory.decodeByteArray( decryptedBytes, 0, decryptedBytes.size ) Image( bitmap = bitmap.asImageBitmap(), contentDescription = null ) }
Good Code
@Composable private fun EncryptedShibaInu( encryptedBytes: ByteArray ) { // THIS IS GOOD // If we use remember with a key instead, the computation // will only be done when the key changes. val bitmap = remember(encryptedBytes){ val decryptedBytes = CryptoManager.decrypt(encryptedBytes) BitmapFactory.decodeByteArray( decryptedBytes, 0, decryptedBytes.size ) } Image( bitmap = bitmap.asImageBitmap(), contentDescription = null ) }
5. Not checking view decomposition strategy in fragments
Bad Code
class YakitoriFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { // THIS IS BAD // Just using ComposeView like this does not ensure // composition is bound to Fragments's lifecycle. // This can lead to state loss under certain circumstances. return ComposeView( requireContext() ).apply { setContent { // Composable } } } }
Good Code
class YakitoriFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return ComposeView( requireContext() ).apply { // THIS IS GOOD // Setting this view docomposition strategy // ties the composition to the Fragment's lifcycle setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed ) setContent { // Composable } } } }
Conclusion
There are a lot more pitfalls when using Jetpack Compose but that's it for this post. Although Jetpack Compose can be fairly complex, it provides a lot of tools to speed up UI development when compared to XML.