Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Adopting Jetpack Compose in Your Existing Proje...

Adopting Jetpack Compose in Your Existing Project - GDG DevFest Bangkok 2024

"Adopting Jetpack Compose in your existing project" at GDG DevFest Bangkok 2024

Somkiat Khitwongwattana

November 17, 2024
Tweet

More Decks by Somkiat Khitwongwattana

Other Decks in Technology

Transcript

  1. class MainActivity : AppCompatActivity() { private val binding: ActivityMainBinding by

    lazy { ... } override fun onCreate(savedInstanceState: Bundle?) { ... binding.button.setOnClickListener { // Do something } } } View binding
  2. class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    ... setContent { ComposeAppTheme { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { Button(onClick = { // Do something }) { Text(text = "Button") Build an UI with Jetpack Compose
  3. setContent { ComposeAppTheme { Box( ... ) { Button(onClick =

    { // Do something }) { Text(text = "Button") } } } } Build an UI with Jetpack Compose
  4. val button1: Button = /* ... */ val button2: Button

    = /* ... */ val button3: Button = /* ... */ val isPurchased = false button1.visibility = if (isPurchased) View.GONE else View.VISIBLE button2.visibility = if (isPurchased) View.GONE else View.VISIBLE button3.visibility = if (isPurchased) View.VISIBLE else View.GONE Readability
  5. val button1: Button = /* ... */ val button2: Button

    = /* ... */ val button3: Button = /* ... */ val isPurchased = true button1.visibility = if (isPurchased) View.GONE else View.VISIBLE button2.visibility = if (isPurchased) View.GONE else View.VISIBLE button3.visibility = if (isPurchased) View.VISIBLE else View.GONE Readability
  6. val isPurchased: Boolean = ... Box( ... ) { if

    (isPurchased) { Button( ... ) { Text("Button 3") } } else { Row( ... ) { Button( ... ) { Text("Button 1") } Spacer( ... ) Button( ... ) { Text("Button 2") } } } } Readability
  7. class ItemViewHolder( private val binding: LayoutListItemBinding ) : ViewHolder(binding.root) {

    fun bind(item: Item) { binding.textViewItem.text = item.text } } Dynamic list UI
  8. class ListAdapter(private val items: List<Item>) : RecyclerView.Adapter<ItemViewHolder>() { override fun

    onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( LayoutListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) override fun getItemCount(): Int = items.size override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { items.getOrNull(position)?.let { holder.bind(it.text) } } } Dynamic list UI
  9. val items: List<Item> = /* ... */ val context: Context

    = /* ... */ with(binding.recyclerView) { layoutManager = LinearLayoutManager( context, LinearLayoutManager.VERTICAL, false, ) adapter = ListAdapter(items) } Dynamic list UI
  10. @Composable fun ListContent(items: List<Item>) { LazyColumn(modifier = Modifier.fillMaxSize()) { items(

    items = items, key = { item -> item.id } ) { item -> Text( modifier = Modifier.fillMaxWidth(), text = item.text, ) } } Dynamic list UI
  11. var activated by remember { mutableStateOf(true) } val contentColor by

    animateColorAsState( ... ) val containerColor by animateColorAsState( ... ) val scale by animateFloatAsState( ... ) Button( modifier = Modifier.scale(scale), colors = ButtonDefaults.buttonColors( containerColor = containerColor, contentColor = contentColor, ), onClick = { activated = !activated }, ) { Text(if (activated) "Activated" else "Inactivated") } Animation
  12. Declarative paradigm shift • Relatively stateless and don't expose setter

    or getter functions • Update the UI by passing the different arguments • Unidirectional data flow
  13. @Composable fun ProfileContent(name: String, status: String, onEditProfile: () -> Unit)

    { Row { Box{ Image( ... ) IconButton(onClick = onEditProfile) { ... } } Column { Text(name) Text(status) } } } State flows down and event flows up
  14. @Composable fun ProfileContent(name: String, status: String, onEditProfile: () -> Unit)

    { Row { Box{ Image( ... ) IconButton(onClick = onEditProfile) { ... } } Column { Text(name) Text(status) } } } State flows down and event flows up
  15. @Composable fun ProfileContent(name: String, status: String, onEditProfile: () -> Unit)

    { Row { Box{ Image( ... ) IconButton(onClick = onEditProfile) { ... } } Column { Text(name) Text(status) } } } State flows down and event flows up
  16. @Composable fun ProfileContent(name: String, status: String, onEditProfile: () -> Unit)

    { Row { Box{ Image( ... ) IconButton(onClick = onEditProfile) { ... } } Column { Text(name) Text(status) } } } State flows down and event flows up
  17. Chance for the big improvement Animate with a few lines

    of code Powerful preview in Android Studio Testable without view ID Friendly for sub-component creation Benefit for teams
  18. @Composable fun MainScreen( ... ) { Column( ... ) {

    Profile( modifier = Modifier.testTag("profile"), ... ) ... } } Testable without view ID
  19. <node index="0" text="" resource-id="" class="android.view.View" package="com.akexorcist.devfest" content-desc="" checkable="false" checked="false" clickable="false"

    enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,66][79,132]"> <node index="0" text="Android" resource-id="" class="android.widget.TextView" package="com.akexorcist.devfest" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,66][79,132]" /> </node> Testable without view ID
  20. <node index="0" text="" resource-id="" class="android.view.View" package="com.akexorcist.devfest" content-desc="" checkable="false" checked="false" clickable="false"

    enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,66][79,132]"> <node index="0" text="Android" resource-id="" class="android.widget.TextView" package="com.akexorcist.devfest" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,66][79,132]" /> </node> Testable without view ID
  21. <node index="0" text="" resource-id="" class="android.view.View" package="com.akexorcist.devfest" content-desc="" checkable="false" checked="false" clickable="false"

    enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,66][79,132]"> <node index="0" text="Android" resource-id="" class="android.widget.TextView" package="com.akexorcist.devfest" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,66][79,132]" /> </node> Testable without view ID
  22. @Composable fun FirstSection(viewModel: FirstViewModel = koinViewModel()) { ... } @Composable

    fun SecondSection(viewModel: SecondViewModel = koinViewModel()) { ... } @Composable fun ThirdSection(viewModel: ThirdViewModel = koinViewModel()) { ... } @Composable fun FourthSection(viewModel: FourthViewModel = koinViewModel()) { ... } Friendly for sub-component creation
  23. @Composable fun MainScreen(viewModel: MainViewModel = koinViewModel()) { Column { FirstSection()

    SecondSection() ThirdSection() FourthSection() } } Friendly for sub-component creation
  24. @Composable fun MainScreen(viewModel: MainViewModel = koinViewModel()) { LazyColumn { item(key

    = "first") { FirstSection() } item(key = "second") { SecondSection() } item(key = "third") { ThirdSection() } item(key = "fourth") { FourthSection() } } } Friendly for sub-component creation
  25. Things to know Debug build performance Profile installer for better

    performance No cost from XML layout inflation Efficiently handle deep UI trees Smart recompositions
  26. APK Size Based on information from the Sunflower app Views

    only 2,252 KB 3,034 KB 2,966 KB Mixed Views and Compose Compose-only
  27. Build Time Based on information from the Sunflower app Views

    only 299.47 ms 399.09 ms 342.16 ms Mixed Views and Compose Compose-only
  28. Based on information from the Tivi app v1.0.0-rc01 APK Size

    Views only 4.14 MB 2.71 MB 2.32 MB Fragment and Compose Compose-only
  29. Method Count Based on information from the Tivi app v1.0.0-rc01

    Views only 40,029 35,165 23,689 Fragment and Compose Compose-only
  30. In the other side Different knowledge Few performance overhead based

    on complexity Fewer third-party libraries compared to the View Limited legacy support
  31. class MainActivity: ComponentActivity() { override fun onCreate( ... ) {

    ... setContent { // In Compose world } } } Compose in Activity
  32. class HomeFragment : Fragment() { override fun onCreateView( ... ):

    View { return ComposeView(requireContext()).apply { setViewCompositionStrategy( ... ) setContent { // In Compose world } } } } Compose in Fragment
  33. @Composable fun AndroidCustomView(title: String, onClick: () -> Unit) { AndroidView(

    factory = { context -> CustomView(context).apply { setOnClickListener(onClick) } }, update = { view -> view.setTitle(title) } ) View in Compose
  34. Codebase preparation for seamless feature development Start with small, incremental

    changes Learning resources for future adopters Negotiate with the product's decision-maker To make it happen
  35. API Maturity Learning resources Libraries & tools Backward compatibility Multiplatform

    supported Jetpack Compose is now production-ready Performance Wide Adoption