Easy XML to Compose Migration

Easy XML to Compose Migration

Hello, In this article I'll be showing you how to convert an already existing XML project to a Compose Project. It is relatively simple to convert an existing XML project to a Compose project. In most cases, you only need to change your project's build script to use the Compose plugin rather than the XML plugin. However, there are a few things to consider before making the switch. I'll walk you through a step-by-step process that you can use in any project.

To begin, we'll need to navigate to our build.gradle file. To enable the use of compose in our XML project, we must add the compose dependencies to the Gradle file. Because the Compose plugin employs a different dependency management system than the XML plugin, your dependencies must be updated accordingly. We also need to add a config block and enable compose to be true. To avoid problems, copy and paste the code below into your project, then hit sync.

// Config block
buildFeatures{
          compose true
}
composeOptions{
         kotlinCompilerExtensionVersion "1.3.2"
}
// Dependencies
dependencies{
       implementation "androidx.compose.material:material:1.3.0-rc01"
       implementation "androidx.compose.compiler:compiler:1.3.2"
       implementation "androidx.compose.ui:ui-tooling-preview:1.3.0-rc01"
       implementation "androidx.compose.activity:activity-compose:1.6.0"
       debugImplementation "androidx.compose.ui:ui-tooling:1.3.0-rc01"
       ....
}

Moving on, there are various approaches to consider when migrating from XML to Compose. One example would be when we are working on a complex project with multiple screens and a lot of views and logic. We can then take it one step at a time by migrating a single UI component from that screen and utilizing a compose view. Assume we have a button that we want to convert into a compose view. The button can then be replaced with a compose view, with the content of the compose view set to a button, while the other views on the screen are ignored.

// Before
< Button 
         android:id="@+id/button_compose"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         .... />
// After
<androidx.compose.ui.platform.ComposeView
         android:id="@+id/button_compose"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         .... />

Then, in our SecondFragment.kt file, navigate to the onViewCreated method. using viewBinding(assuming this is what you're currently making use of), we get a reference to our to compose view and set whatever content we like on it. In this case, a button.

binding.buttonCompose.setContent{
       Button(onClick = {
              // Navigate to first fragment screen
              findNavController().navigate(R.id.action_SecondFragment_to_firstFragment)
       })  {
              // Set a text to our button
              Text(text = "Previous")
       }
}

That is it for our first approach, as you can see, is extremely simple. Run the app and look at the second screen, There you'll find a button with click functionality. Now, let's take a look at another approach to migrating our XML to compose

Another option is to remove the viewBinding features. You migrate each screen to compose. To accomplish this, navigate to your project package and create a new file called FirstScreen.kt that takes in two parameters and is annotated using Composable. (Note: You can name your file whatever the name of the screen is).

@Composable
fun FirstScreen(
      firstState: FirstState
      onNextClick: () -> Unit
){
}

According to the code, our FirstScreen.kt file accept two parameters. The first is the state, which simply represents the current state of the screen. On the other hand, we will simply reveal our actions. The actions, which is a Lambda refer to what the user can do while on that specific screen. In order to simulate a real-world example, we will create a screen state that emits a state flow in our ViewModel file. I won't go into detail about its ViewModel, but you can see what it looks like by referencing the code below.

class FirstViewModel: ViewModel() {
        private val _state = MutableStateFlow(FirstState)
        val state = _state.asStateFlow()
}

class FirstState

Now, we specify whatever view we'll like to display on our screen in compose. It's a very simple screen but the idea is knowing what steps we take when migrating XML to compose

@Composable
fun FirstScreen(
      firstState: FirstState
      onNextClick: () -> Unit
){
    Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.SpaceEvenly,
            modifier = Alignment.CenterHorizontally,
    ) {
         Text(text = "Hello first fragment")
         Button(onClick= onNextClick) {
                Text(text = "Next")
         }
    }
}

Now that we've cleared that up, let's move on to our fragment, which in our case is FirstFragment. Because we will not be using our viewBinding, you can safely delete it along with the code in the onCreateView method. Now we'll create a compose view (A compose view is a view where we can set composable content) that will be returned by our onCreateView method.

....
        private lateinit var composeView : ComposeView

        override fun onCreateView(
        ....
        ): View {
             return ComposeView(requireContext()).also {
                  composeView = it
             }
}

We want to set the content of our compose view in our onViewCreated method, and all we'll be doing is setting the FirstScreen. We would also need to assign a state and a onNextClick event. To pass in the state, we would need to listen for our state from the ViewModel.

...
    private val viewModel: FirstViewModel by viewModels()
...

    composeView.setContent{
            val state by viewModel.state.collectAsState()
            FirstScreen(
                   state = state,
                   onNextClick = {
                         findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
                   } 
           )
    }

We still use fragments in order to keep our navigation as a single source. This allows you to get a better sense of which screens you're on and also makes screen transitioning easier. Now all we have to do is delete our first fragment XML file. When you launch the app, you'll see our compose-only view with a textview and a button.

That's all you really need to know about migrating an XML project to compose. Following these steps, you can easily migrate any of your XML projects to compose. Thank you for reading. Bye-Bye