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

From XML To Compose: My Journey of migrating an...

From XML To Compose: My Journey of migrating an existing large Android app to Jetpack Compose

After Jetpack Compose was finally released, creators of new Android apps were now equipped with a tool that will help them easily write Android apps using the new declarative approach. The journey is not easy for developers who already have large apps that use the imperative, XML-based approach and want to convert it to Jetpack Compose. The migration journey has its ups and downs, initially daunting but eventually satisfying. This talk will highlight that journey, my learnings during the process, the benefits, pitfalls to avoid, and advice for developers looking to migrate their existing Android apps to Jetpack Compose as well as why it is worthwhile to migrate an existing app to Jetpack Compose.

Ahmed Tikiwa

March 04, 2022
Tweet

More Decks by Ahmed Tikiwa

Other Decks in Programming

Transcript

  1. FROM XML TO COMPOSE MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE AHMED TIKIWA | SENIOR SOFTWARE ENGINEER - ANDROID @ LUNO | @AKITIKKX PHOTO BY ILYA PAVLOV ON UNSPLASH
  2. “It’s much easier to trace through code when it’s all

    written in the same language [Kotlin] and often the same f ile, rather than jumping back and forth between Kotlin and XML” - Monzo WHAT ARE OTHER COMPANIES SAYING? FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA
  3. “Our theming layer is vastly more intuitive and legible. We’ve

    been able to accomplish within a single Kotlin f ile what otherwise extended across multiple XML f iles that were responsible for attribute de f initions and assignments via multiple layered theme overlays.” - Twitter WHAT ARE OTHER COMPANIES SAYING? FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA
  4. Announced at Google I/O 2019 as a preview “One of

    the areas we never solved was UI. We really wanted to look at how could you make it super simple to develop UI.” 
 - Karen Ng, Group Product Manager at Google “What I think is once people start seeing Compose in action, it really becomes a delightful thing to program” 
 - Leland Richardson, Software Engineer at Google Excited about new library SOURCE: HTTPS://EVENTS.GOOGLE.COM/IO2019/RECAP/ WHEN WAS JETPACK COMPOSE INTRODUCED FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA
  5. LEARNING COMPOSE FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING

    AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA
  6. LEARNING COMPOSE FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING

    AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA
  7. LEARNING COMPOSE FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING

    AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA
  8. Created in 2015, available on Google Play Store Open-source, o

    ff icially part of Google Developers Dev Library Dashboard screen Search screen Explore Screen Show Detail Screen Seasons list Episodes list Trakt account screen SOURCE: HTTPS://EVENTS.GOOGLE.COM/IO2019/RECAP/ ABOUT THE APP FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA
  9. The bottom-up approach starts migrating the smaller UI elements on

    the screen, like a Button or a TextView, followed by its ViewGroup elements until everything is converted to composable functions. 
 The top-down approach starts migrating the fragments or view containers, like a FrameLayout, ConstraintLayout, or RecyclerView, followed by the smaller UI elements on the screen. ADOPTING COMPOSE: CHOOSING THE APPROACH FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA
  10. Each fragment has an associated XML layout Gradual migration Use

    of ComposeView Introduce Compose UI content into XML layout A container WHAT DOES INTEROPERABILITY LOOK LIKE FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA
  11. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA WHAT DOES INTEROPERABILITY LOOK LIKE Removed TextViews RecyclerViews NestedScrollView ConstraintLayout LinearProgressIndicator Added ComposeView <layout ...> <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_container" android:layout_width="match_parent" android:layout_height="match_parent" android:transitionGroup="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </layout> /res/layout/fragment_search.xml
  12. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA WHAT DOES INTEROPERABILITY LOOK LIKE @OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) override fun onCreateView(...): View { ... binding.composeContainer.apply { setViewCompositionStrategy( ViewCompositionStrategy .DisposeOnViewTreeLifecycleDestroyed ) setContent { MdcTheme { SearchScreen(navController = findNavController()) } } } return binding.root } /ui/search/SearchFragment.kt
  13. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA WHAT DOES INTEROPERABILITY LOOK LIKE @OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) override fun onCreateView(...): View { ... binding.composeContainer.apply { setViewCompositionStrategy( ViewCompositionStrategy .DisposeOnViewTreeLifecycleDestroyed ) setContent { MdcTheme { SearchScreen(navController = findNavController()) } } } return binding.root } /ui/search/SearchFragment.kt
  14. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA WHAT DOES INTEROPERABILITY LOOK LIKE @OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) override fun onCreateView(...): View { ... binding.composeContainer.apply { setViewCompositionStrategy( ViewCompositionStrategy .DisposeOnViewTreeLifecycleDestroyed ) setContent { MdcTheme { SearchScreen(navController = findNavController()) } } } return binding.root } /ui/search/SearchFragment.kt
  15. BREAKING DOWN THE CHANGES: SEARCH SCREEN Focus on: Search bar

    Search results list The search result item Displaying the list Removing RecyclerView adapter, ViewHolder and item layout FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA
  16. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @ExperimentalMaterialApi @ExperimentalComposeUiApi @Composable fun SearchScreen( viewModel: SearchViewModel = hiltViewModel(), navController: NavController ) { ... } /ui/search/SearchScreen.kt
  17. Built around composable functions De f ine app’s UI Provide

    data to be displayed No more focus on UI construction process WHAT IS A COMPOSABLE?
  18. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @ExperimentalMaterialApi @ExperimentalComposeUiApi @Composable fun SearchScreen( viewModel: SearchViewModel = hiltViewModel(), navController: NavController ) { val searchResultsList = viewModel.searchResponse.observeAsState() val isLoading = viewModel.isLoading.observeAsState() Surface(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier.padding(8.dp) ) { ... } } } /ui/search/SearchScreen.kt
  19. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @ExperimentalMaterialApi @ExperimentalComposeUiApi @Composable fun SearchScreen( viewModel: SearchViewModel = hiltViewModel(), navController: NavController ) { val searchResultsList = viewModel.searchResponse.observeAsState() val isLoading = viewModel.isLoading.observeAsState() Surface(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier.padding(8.dp) ) { ... } } } /ui/search/SearchScreen.kt
  20. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @ExperimentalMaterialApi @ExperimentalComposeUiApi @Composable fun SearchScreen( viewModel: SearchViewModel = hiltViewModel(), navController: NavController ) { val searchResultsList = viewModel.searchResponse.observeAsState() val isLoading = viewModel.isLoading.observeAsState() Surface(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier.padding(8.dp) ) { ... } } } /ui/search/SearchScreen.kt
  21. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @ExperimentalMaterialApi @ExperimentalComposeUiApi @Composable fun SearchScreen( viewModel: SearchViewModel = hiltViewModel(), navController: NavController ) { val searchResultsList = viewModel.searchResponse.observeAsState() val isLoading = viewModel.isLoading.observeAsState() Surface(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier.padding(8.dp) ) { ... } } } /ui/search/SearchScreen.kt
  22. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @ExperimentalMaterialApi @ExperimentalComposeUiApi @Composable fun SearchScreen( viewModel: SearchViewModel = hiltViewModel(), navController: NavController ) { val searchResultsList = viewModel.searchResponse.observeAsState() val isLoading = viewModel.isLoading.observeAsState() Surface(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier.padding(8.dp) ) { ... } } } /ui/search/SearchScreen.kt
  23. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @ExperimentalMaterialApi @ExperimentalComposeUiApi @Composable fun SearchScreen( viewModel: SearchViewModel = hiltViewModel(), navController: NavController ) { val searchResultsList = viewModel.searchResponse.observeAsState() val isLoading = viewModel.isLoading.observeAsState() Surface(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier.padding(8.dp) ) { ... } } } /ui/search/SearchScreen.kt
  24. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @ExperimentalMaterialApi @ExperimentalComposeUiApi @Composable fun SearchScreen(...) { ... Surface(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier.padding(8.dp) ) { Box(modifier = Modifier.fillMaxSize()) { ... } } } } /ui/search/SearchScreen.kt
  25. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @ExperimentalMaterialApi @ExperimentalComposeUiApi @Composable fun SearchScreen(...) { ... Box(modifier = Modifier.fillMaxSize()) { SearchArea() if (isLoading.value == true) { LinearProgressIndicator( modifier = Modifier .padding(8.dp) .fillMaxWidth() ) } } } } /ui/search/SearchScreen.kt
  26. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @ExperimentalMaterialApi @ExperimentalComposeUiApi @Composable fun SearchScreen(...) { ... Box(modifier = Modifier.fillMaxSize()) { SearchArea() if (isLoading.value == true) { LinearProgressIndicator( modifier = Modifier .padding(8.dp) .fillMaxWidth() ) } } } } /ui/search/SearchScreen.kt
  27. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class) @Composable fun SearchArea( searchResultsList: List<ShowSearch>?, onTextSubmit: (query: String) -> Unit, onResultClick: (item: ShowSearch) -> Unit ) { Column(modifier = Modifier.padding(top = 8.dp)) { SearchForm { onTextSubmit(it) } searchResultsList?.let { results -> SearchResultsList(list = results) { onResultClick(it) } } } } /ui/search/SearchScreen.kt
  28. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class) @Composable fun SearchArea( searchResultsList: List<ShowSearch>?, onTextSubmit: (query: String) -> Unit, onResultClick: (item: ShowSearch) -> Unit ) { Column(modifier = Modifier.padding(top = 8.dp)) { SearchForm { onTextSubmit(it) } searchResultsList?.let { results -> SearchResultsList(list = results) { onResultClick(it) } } } } /ui/search/SearchScreen.kt
  29. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class) @Composable fun SearchArea( searchResultsList: List<ShowSearch>?, onTextSubmit: (query: String) -> Unit, onResultClick: (item: ShowSearch) -> Unit ) { Column(modifier = Modifier.padding(top = 8.dp)) { SearchForm { onTextSubmit(it) } searchResultsList?.let { results -> SearchResultsList(list = results) { onResultClick(it) } } } } /ui/search/SearchScreen.kt
  30. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class) @Composable fun SearchArea( searchResultsList: List<ShowSearch>?, onTextSubmit: (query: String) -> Unit, onResultClick: (item: ShowSearch) -> Unit ) { Column(modifier = Modifier.padding(top = 8.dp)) { SearchForm { onTextSubmit(it) } searchResultsList?.let { results -> SearchResultsList(list = results) { onResultClick(it) } } } } /ui/search/SearchScreen.kt
  31. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class) @Composable fun SearchArea( searchResultsList: List<ShowSearch>?, onTextSubmit: (query: String) -> Unit, onResultClick: (item: ShowSearch) -> Unit ) { Column(modifier = Modifier.padding(top = 8.dp)) { SearchForm { onTextSubmit(it) } searchResultsList?.let { results -> SearchResultsList(list = results) { onResultClick(it) } } } } /ui/search/SearchScreen.kt @ExperimentalComposeUiApi @Composable fun SearchForm( onSearch: (String) -> Unit ) { val searchQueryState = rememberSaveable { mutableStateOf("") } SearchInputField( inputLabel = stringResource(id = R.string.search_input_hint), valueState = searchQueryState, onValueChange = { onSearch(searchQueryState.value.trim()) } ) }
  32. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class) @Composable fun SearchArea( searchResultsList: List<ShowSearch>?, onTextSubmit: (query: String) -> Unit, onResultClick: (item: ShowSearch) -> Unit ) { Column(modifier = Modifier.padding(top = 8.dp)) { SearchForm { onTextSubmit(it) } searchResultsList?.let { results -> SearchResultsList(list = results) { onResultClick(it) } } } } /ui/search/SearchScreen.kt @ExperimentalComposeUiApi @Composable fun SearchForm( onSearch: (String) -> Unit ) { val searchQueryState = rememberSaveable { mutableStateOf("") } SearchInputField( inputLabel = stringResource(id = R.string.search_input_hint), valueState = searchQueryState, onValueChange = { onSearch(searchQueryState.value.trim()) } ) }
  33. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class) @Composable fun SearchArea( searchResultsList: List<ShowSearch>?, onTextSubmit: (query: String) -> Unit, onResultClick: (item: ShowSearch) -> Unit ) { Column(modifier = Modifier.padding(top = 8.dp)) { SearchForm { onTextSubmit(it) } searchResultsList?.let { results -> SearchResultsList(list = results) { onResultClick(it) } } } } /ui/search/SearchScreen.kt @ExperimentalComposeUiApi @Composable fun SearchForm( onSearch: (String) -> Unit ) { val searchQueryState = rememberSaveable { mutableStateOf("") } SearchInputField( inputLabel = stringResource(id = R.string.search_input_hint), valueState = searchQueryState, onValueChange = { onSearch(searchQueryState.value.trim()) } ) }
  34. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class) @Composable fun SearchArea( searchResultsList: List<ShowSearch>?, onTextSubmit: (query: String) -> Unit, onResultClick: (item: ShowSearch) -> Unit ) { Column(modifier = Modifier.padding(top = 8.dp)) { SearchForm { onTextSubmit(it) } searchResultsList?.let { results -> SearchResultsList(list = results) { onResultClick(it) } } } } /ui/search/SearchScreen.kt @ExperimentalComposeUiApi @Composable fun SearchForm( onSearch: (String) -> Unit ) { val searchQueryState = rememberSaveable { mutableStateOf("") } SearchInputField( inputLabel = stringResource(id = R.string.search_input_hint), valueState = searchQueryState, onValueChange = { onSearch(searchQueryState.value.trim()) } ) }
  35. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class) @Composable fun SearchArea( searchResultsList: List<ShowSearch>?, onTextSubmit: (query: String) -> Unit, onResultClick: (item: ShowSearch) -> Unit ) { Column(modifier = Modifier.padding(top = 8.dp)) { SearchForm { onTextSubmit(it) } searchResultsList?.let { results -> SearchResultsList(list = results) { onResultClick(it) } } } } /ui/search/SearchScreen.kt @ExperimentalComposeUiApi @Composable fun SearchForm( onSearch: (String) -> Unit ) { val searchQueryState = rememberSaveable { mutableStateOf("") } SearchInputField( inputLabel = stringResource(id = R.string.search_input_hint), valueState = searchQueryState, onValueChange = { onSearch(searchQueryState.value.trim()) } ) }
  36. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @ExperimentalComposeUiApi @Composable fun SearchForm( onSearch: (String) -> Unit ) { val searchQueryState = rememberSaveable { mutableStateOf("") } SearchInputField( inputLabel = stringResource(id = R.string.search_input_hint), valueState = searchQueryState, onValueChange = { onSearch(searchQueryState.value.trim()) } ) } /ui/search/SearchScreen.kt @Composable fun SearchInputField( modifier: Modifier = Modifier, inputLabel: String, valueState: MutableState<String>, onValueChange: (value: String) -> Unit ) { OutlinedTextField( value = valueState.value, onValueChange = { valueState.value = it onValueChange(valueState.value) }, label = { Text(inputLabel) }, singleLine = true, modifier = modifier .padding(8.dp) .fillMaxWidth() ) }
  37. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN /ui/search/SearchScreen.kt @Composable fun SearchInputField( modifier: Modifier = Modifier, inputLabel: String, valueState: MutableState<String>, onValueChange: (value: String) -> Unit ) { OutlinedTextField( value = valueState.value, onValueChange = { valueState.value = it onValueChange(valueState.value) }, label = { Text(inputLabel) }, singleLine = true, modifier = modifier .padding(8.dp) .fillMaxWidth() ) } @ExperimentalComposeUiApi @Composable fun SearchForm( onSearch: (String) -> Unit ) { val searchQueryState = rememberSaveable { mutableStateOf("") } SearchInputField( inputLabel = stringResource(id = R.string.search_input_hint), valueState = searchQueryState, onValueChange = { onSearch(searchQueryState.value.trim()) } ) }
  38. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN /ui/search/SearchScreen.kt @Composable fun SearchInputField( modifier: Modifier = Modifier, inputLabel: String, valueState: MutableState<String>, onValueChange: (value: String) -> Unit ) { OutlinedTextField( value = valueState.value, onValueChange = { valueState.value = it onValueChange(valueState.value) }, label = { Text(inputLabel) }, singleLine = true, modifier = modifier .padding(8.dp) .fillMaxWidth() ) } @ExperimentalComposeUiApi @Composable fun SearchForm( onSearch: (String) -> Unit ) { val searchQueryState = rememberSaveable { mutableStateOf("") } SearchInputField( inputLabel = stringResource(id = R.string.search_input_hint), valueState = searchQueryState, onValueChange = { onSearch(searchQueryState.value.trim()) } ) }
  39. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class) @Composable fun SearchArea( searchResultsList: List<ShowSearch>?, onTextSubmit: (query: String) -> Unit, onResultClick: (item: ShowSearch) -> Unit ) { Column(modifier = Modifier.padding(top = 8.dp)) { SearchForm { onTextSubmit(it) } searchResultsList?.let { results -> SearchResultsList(list = results) { onResultClick(it) } } } } /ui/search/SearchScreen.kt @ExperimentalMaterialApi @Composable fun SearchResultsList( list: List<ShowSearch>, onClick: (item: ShowSearch) -> Unit ) { LazyColumn { items(list) { SearchListCard(item = it) { onClick(it) } } } }
  40. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class) @Composable fun SearchArea( searchResultsList: List<ShowSearch>?, onTextSubmit: (query: String) -> Unit, onResultClick: (item: ShowSearch) -> Unit ) { Column(modifier = Modifier.padding(top = 8.dp)) { SearchForm { onTextSubmit(it) } searchResultsList?.let { results -> SearchResultsList(list = results) { onResultClick(it) } } } } /ui/search/SearchScreen.kt @ExperimentalMaterialApi @Composable fun SearchResultsList( list: List<ShowSearch>, onClick: (item: ShowSearch) -> Unit ) { LazyColumn { items(list) { SearchListCard(item = it) { onClick(it) } } } }
  41. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @ExperimentalMaterialApi @ExperimentalComposeUiApi @Composable fun SearchScreen(...) { ... Box(modifier = Modifier.fillMaxSize()) { SearchArea() if (isLoading.value == true) { LinearProgressIndicator( modifier = Modifier .padding(8.dp) .fillMaxWidth() ) } } } } /ui/search/SearchScreen.kt SearchArea( searchResultsList = searchResultsList.value, onResultClick = { val directions = SearchFragmentDirections.actionSearchFragmentToSh owDetailFragment( ShowDetailArg( source = "search", showId = it.id, showTitle = it.name, showImageUrl = it.originalImageUrl, showBackgroundUrl = it.mediumImageUrl ) ) navController.navigate(directions) }, onTextSubmit = { viewModel.onQueryTextSubmit(it) } )
  42. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @ExperimentalMaterialApi @ExperimentalComposeUiApi @Composable fun SearchScreen(...) { ... Box(modifier = Modifier.fillMaxSize()) { SearchArea() if (isLoading.value == true) { LinearProgressIndicator( modifier = Modifier .padding(8.dp) .fillMaxWidth() ) } } } } /ui/search/SearchScreen.kt SearchArea( searchResultsList = searchResultsList.value, onResultClick = { val directions = SearchFragmentDirections.actionSearchFragmentToSh owDetailFragment( ShowDetailArg( source = "search", showId = it.id, showTitle = it.name, showImageUrl = it.originalImageUrl, showBackgroundUrl = it.mediumImageUrl ) ) navController.navigate(directions) }, onTextSubmit = { viewModel.onQueryTextSubmit(it) } )
  43. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA BREAKING DOWN THE CHANGES: SEARCH SCREEN @ExperimentalMaterialApi @ExperimentalComposeUiApi @Composable fun SearchScreen(...) { ... Box(modifier = Modifier.fillMaxSize()) { SearchArea() if (isLoading.value == true) { LinearProgressIndicator( modifier = Modifier .padding(8.dp) .fillMaxWidth() ) } } } } /ui/search/SearchScreen.kt SearchArea( searchResultsList = searchResultsList.value, onResultClick = { val directions = SearchFragmentDirections.actionSearchFragmentToSh owDetailFragment( ShowDetailArg( source = "search", showId = it.id, showTitle = it.name, showImageUrl = it.originalImageUrl, showBackgroundUrl = it.mediumImageUrl ) ) navController.navigate(directions) }, onTextSubmit = { viewModel.onQueryTextSubmit(it) } )
  44. BEFORE AND AFTER: SEARCH SCREEN FROM XML TO COMPOSE, MY

    JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA SearchFragment.kt @Composable SearchScreen() @Composable 
 SearchForm() @Composable 
 SearchResultsList()
  45. BEFORE AND AFTER: DASHBOARD SCREEN FROM XML TO COMPOSE, MY

    JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA DashboardFragment.kt @Composable DashboardScreen() @Composable 
 ShowsRow() 
 & uses LazyRow() @Composable 
 ShowsRow() 
 & uses LazyRow()
  46. BEFORE AND AFTER: EXPLORE SCREEN FROM XML TO COMPOSE, MY

    JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA ExploreFragment.kt @Composable ExploreScreen @Composable 
 TrendingShowsRow() 
 & uses LazyRow() @Composable 
 PopularShowsRow() 
 & uses LazyRow
  47. BEFORE AND AFTER: SHOW DETAIL SCREEN FROM XML TO COMPOSE,

    MY JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA ShowDetailFragment.kt @Composable ShowDetailScreen @Composable 
 BackdropAndTitle() @Composable 
 PosterAndMetadata() @Composable 
 Text()
  48. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA ShowDetailFragment.kt @Composable ShowDetailScreen @Composable 
 PreviousEpisode() @Composable 
 ShowDetailButtons @Composable 
 ShowCastList() 
 & uses LazyRow() BEFORE AND AFTER: SHOW DETAIL SCREEN
  49. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA ShowDetailFragment.kt @Composable ShowDetailScreen @Composable 
 PreviousEpisode() @Composable 
 TraktRatingSummary() BEFORE AND AFTER: SHOW DETAIL SCREEN
  50. BREAKING DOWN THE CHANGES: SEARCH SCREEN FROM XML TO COMPOSE,

    MY JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA @Composable ShowSeasonEpisodesScreen ShowSeasonsEpisodesFragment.kt @Composable 
 SectionHeadingText() @Composable 
 ShowSeasonEpisodes() 
 & uses LazyColumn() @Composable 
 ShowSeasonEpisodeCard()
  51. BREAKING DOWN THE CHANGES: SEARCH SCREEN FROM XML TO COMPOSE,

    MY JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA TraktAccountFragment.kt @Composable TraktAccountScreen() @Composable 
 FavoritesList() 
 & uses LazyVerticalGrid() @Composable 
 ListPosterCard() @Composable 
 SectionHeadingText()
  52. WHAT STILL NEEDS TO BE UPDATED OR CONVERTED FROM XML

    TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA Toolbar Bottom navigation bar Migration to Compose Navigation Removal of all fragment f iles Replace Surface with Sca ff old Replacement of observeAsState with MutableState observation Add animations Add tests for Composables
  53. O ff icial Compose Documentation 
 https://developer.android.com/jetpack/compose O ff icial

    Compose course 
 https://developer.android.com/courses/pathways/compose Compose Layout Basics https://developer.android.com/jetpack/compose/layouts/basics State Hoisting https://developer.android.com/jetpack/compose/state#state-hoisting UpNext TV Series Manager code is available as an open-source project and part of the Google Developers Dev Library https://devlibrary.withgoogle.com/products/android/repos/akitikkx-upnext. Click on the “View on Github” to see the code. Contributions welcome from the community! Please read my Readme and contribution Guidelines for more information RESOURCES FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING LARGE APP TO JETPACK COMPOSE - AHMED TIKIWA
  54. FROM XML TO COMPOSE, MY JOURNEY OF TRANSFORMING AN EXISTING

    LARGE APP TO JETPACK COMPOSE AHMED TIKIWA SENIOR SOFTWARE ENGINEER - ANDROID @ LUNO @ahmed_tikiwa