ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

Five Common Pitfalls when using Jetpack Compse

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.

unifa-e.com