February 18, 2025


Jetpack Compose is Android’s fashionable toolkit for constructing native UI. It simplifies and accelerates UI growth by utilizing a declarative strategy, which is a major shift from the standard crucial XML-based layouts. If in case you have an current Android app written in Kotlin utilizing the MVP (Mannequin-View-Presenter) sample with XML layouts, fragments, and actions, migrating to Jetpack Compose can convey quite a few advantages, together with improved developer productiveness, diminished boilerplate code, and a extra fashionable UI structure.

On this article, we’ll stroll by the steps emigrate an Android app from MVP with XML layouts to Jetpack Compose. We’ll use a fundamental Information App to elucidate intimately learn how to migrate all layers of the app. The app has two screens:

  1. A Information Listing Fragment to show a listing of reports objects.
  2. A Information Element Fragment to indicate the small print of a particular information merchandise.

We’ll begin by exhibiting the unique MVP implementation, together with the Presenters, after which migrate the app to Jetpack Compose step-by-step. We’ll additionally add error dealing with, loading states, and use Kotlin Move as a substitute of LiveData for a extra fashionable and reactive strategy.

1. Perceive the Key Variations

Earlier than diving into the migration, it’s important to know the important thing variations between the 2 approaches:

  • Crucial vs. Declarative UI: XML layouts are crucial, that means you outline the UI construction after which manipulate it programmatically. Jetpack Compose is declarative, that means you describe what the UI ought to appear to be for any given state, and Compose handles the rendering.
  • MVP vs. Compose Structure: MVP separates the UI logic into Presenters and Views. Jetpack Compose encourages a extra reactive and state-driven structure, typically utilizing ViewModel and State Hoisting.
  • Fragments and Actions: In conventional Android growth, Fragments and Actions are used to handle UI elements. In Jetpack Compose, you possibly can substitute most Fragments and Actions with composable features.

2. Plan the Migration

Migrating a whole app to Jetpack Compose could be a vital enterprise. Right here’s a advised strategy:

  1. Begin Small: Start by migrating a single display or part to Jetpack Compose. This may assist you perceive the method and establish potential challenges.
  2. Incremental Migration: Jetpack Compose is designed to work alongside conventional Views, so you possibly can migrate your app incrementally. Use ComposeView in XML layouts or AndroidView in Compose to bridge the hole.
  3. Refactor MVP to MVVM: Jetpack Compose works effectively with the MVVM (Mannequin-View-ViewModel) sample. Contemplate refactoring your Presenters into ViewModels.
  4. Change Fragments with Composable Capabilities: Fragments may be changed with composable features, simplifying navigation and UI administration.
  5. Add Error Dealing with and Loading States: Guarantee your app handles errors gracefully and shows loading states throughout knowledge fetching.
  6. Use Kotlin Move: Change LiveData with Kotlin Move for a extra fashionable and reactive strategy.

3. Set Up Jetpack Compose

Earlier than beginning the migration, guarantee your challenge is about up for Jetpack Compose:

  1. Replace Gradle Dependencies:
    Add the mandatory Compose dependencies to your construct.gradle file:
    android {
        ...
        buildFeatures {
            compose true
        }
        composeOptions {
            kotlinCompilerExtensionVersion '1.5.3'
        }
    }
    
    dependencies {
        implementation 'androidx.exercise:activity-compose:1.8.0'
        implementation 'androidx.compose.ui:ui:1.5.4'
        implementation 'androidx.compose.materials:materials:1.5.4'
        implementation 'androidx.compose.ui:ui-tooling-preview:1.5.4'
        implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2'
        implementation 'androidx.navigation:navigation-compose:2.7.4' // For navigation
        implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' // For Move
        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' // For Move
    }
  2. Allow Compose in Your Mission:
    Guarantee your challenge is utilizing the right Kotlin and Android Gradle plugin variations.

4. Unique MVP Implementation

a. Information Listing Fragment and Presenter

The NewsListFragment shows a listing of reports objects. The NewsListPresenter fetches the info and updates the view.

NewsListFragment.kt

class NewsListFragment : Fragment(), NewsListView {

    non-public lateinit var presenter: NewsListPresenter
    non-public lateinit var adapter: NewsListAdapter

    override enjoyable onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.format.fragment_news_list, container, false)
        val recyclerView = view.findViewById(R.id.recyclerView)
        adapter = NewsListAdapter { newsItem -> presenter.onNewsItemClicked(newsItem) }
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(context)
        presenter = NewsListPresenter(this)
        presenter.loadNews()
        return view
    }

    override enjoyable showNews(information: Listing) {
        adapter.submitList(information)
    }

    override enjoyable showLoading() {
        // Present loading indicator
    }

    override enjoyable showError(error: String) {
        // Present error message
    }
}

NewsListPresenter.kt

class NewsListPresenter(non-public val view: NewsListView) {

    enjoyable loadNews() {
        view.showLoading()
        // Simulate fetching information from an information supply (e.g., API or native database)
        attempt {
            val newsList = listOf(
                NewsItem(id = 1, title = "Information 1", abstract = "Abstract 1"),
                NewsItem(id = 2, title = "Information 2", abstract = "Abstract 2")
            )
            view.showNews(newsList)
        } catch (e: Exception) {
            view.showError(e.message ?: "An error occurred")
        }
    }

    enjoyable onNewsItemClicked(newsItem: NewsItem) {
        // Navigate to the information element display
        val intent = Intent(context, NewsDetailActivity::class.java).apply {
            putExtra("newsId", newsItem.id)
        }
        startActivity(intent)
    }
}

NewsListView.kt

interface NewsListView {
    enjoyable showNews(information: Listing)
    enjoyable showLoading()
    enjoyable showError(error: String)
}

b. Information Element Fragment and Presenter

The NewsDetailFragment shows the small print of a particular information merchandise. The NewsDetailPresenter fetches the small print and updates the view.

NewsDetailFragment.kt

class NewsDetailFragment : Fragment(), NewsDetailView {

    non-public lateinit var presenter: NewsDetailPresenter

    override enjoyable onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.format.fragment_news_detail, container, false)
        presenter = NewsDetailPresenter(this)
        val newsId = arguments?.getInt("newsId") ?: 0
        presenter.loadNewsDetail(newsId)
        return view
    }

    override enjoyable showNewsDetail(newsItem: NewsItem) {
        view?.findViewById(R.id.title)?.textual content = newsItem.title
        view?.findViewById(R.id.abstract)?.textual content = newsItem.abstract
    }

    override enjoyable showLoading() {
        // Present loading indicator
    }

    override enjoyable showError(error: String) {
        // Present error message
    }
}

NewsDetailPresenter.kt

class NewsDetailPresenter(non-public val view: NewsDetailView) {

    enjoyable loadNewsDetail(newsId: Int) {
        view.showLoading()
        // Simulate fetching information element from an information supply (e.g., API or native database)
        attempt {
            val newsItem = NewsItem(id = newsId, title = "Information $newsId", abstract = "Abstract $newsId")
            view.showNewsDetail(newsItem)
        } catch (e: Exception) {
            view.showError(e.message ?: "An error occurred")
        }
    }
}

NewsDetailView.kt

interface NewsDetailView {
    enjoyable showNewsDetail(newsItem: NewsItem)
    enjoyable showLoading()
    enjoyable showError(error: String)
}

5. Migrate to Jetpack Compose

a. Migrate the Information Listing Fragment

Change the NewsListFragment with a composable perform. The NewsListPresenter shall be refactored right into a NewsListViewModel.

NewsListScreen.kt

@Composable
enjoyable NewsListScreen(viewModel: NewsListViewModel, onItemClick: (NewsItem) -> Unit) {
    val newsState by viewModel.newsState.collectAsState()

    when (newsState) {
        is NewsState.Loading -> {
            // Present loading indicator
            CircularProgressIndicator()
        }
        is NewsState.Success -> {
            val information = (newsState as NewsState.Success).information
            LazyColumn {
                objects(information) { newsItem ->
                    NewsListItem(newsItem = newsItem, onClick = { onItemClick(newsItem) })
                }
            }
        }
        is NewsState.Error -> {
            // Present error message
            val error = (newsState as NewsState.Error).error
            Textual content(textual content = error, colour = Shade.Crimson)
        }
    }
}

@Composable
enjoyable NewsListItem(newsItem: NewsItem, onClick: () -> Unit) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
            .clickable { onClick() }
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Textual content(textual content = newsItem.title, type = MaterialTheme.typography.h6)
            Textual content(textual content = newsItem.abstract, type = MaterialTheme.typography.body1)
        }
    }
}

NewsListViewModel.kt

class NewsListViewModel : ViewModel() {

    non-public val _newsState = MutableStateFlow(NewsState.Loading)
    val newsState: StateFlow get() = _newsState

    init {
        loadNews()
    }

    non-public enjoyable loadNews() {
        viewModelScope.launch {
            _newsState.worth = NewsState.Loading
            attempt {
                // Simulate fetching information from an information supply (e.g., API or native database)
                val newsList = listOf(
                    NewsItem(id = 1, title = "Information 1", abstract = "Abstract 1"),
                    NewsItem(id = 2, title = "Information 2", abstract = "Abstract 2")
                )
                _newsState.worth = NewsState.Success(newsList)
            } catch (e: Exception) {
                _newsState.worth = NewsState.Error(e.message ?: "An error occurred")
            }
        }
    }
}

sealed class NewsState {
    object Loading : NewsState()
    knowledge class Success(val information: Listing) : NewsState()
    knowledge class Error(val error: String) : NewsState()
}

b. Migrate the Information Element Fragment

Change the NewsDetailFragment with a composable perform. The NewsDetailPresenter shall be refactored right into a NewsDetailViewModel.

NewsDetailScreen.kt

@Composable
enjoyable NewsDetailScreen(viewModel: NewsDetailViewModel) {
    val newsState by viewModel.newsState.collectAsState()

    when (newsState) {
        is NewsState.Loading -> {
            // Present loading indicator
            CircularProgressIndicator()
        }
        is NewsState.Success -> {
            val newsItem = (newsState as NewsState.Success).information
            Column(modifier = Modifier.padding(16.dp)) {
                Textual content(textual content = newsItem.title, type = MaterialTheme.typography.h4)
                Textual content(textual content = newsItem.abstract, type = MaterialTheme.typography.body1)
            }
        }
        is NewsState.Error -> {
            // Present error message
            val error = (newsState as NewsState.Error).error
            Textual content(textual content = error, colour = Shade.Crimson)
        }
    }
}

NewsDetailViewModel.kt

class NewsDetailViewModel : ViewModel() {

    non-public val _newsState = MutableStateFlow(NewsState.Loading)
    val newsState: StateFlow get() = _newsState

    enjoyable loadNewsDetail(newsId: Int) {
        viewModelScope.launch {
            _newsState.worth = NewsState.Loading
            attempt {
                // Simulate fetching information element from an information supply (e.g., API or native database)
                val newsItem = NewsItem(id = newsId, title = "Information $newsId", abstract = "Abstract $newsId")
                _newsState.worth = NewsState.Success(newsItem)
            } catch (e: Exception) {
                _newsState.worth = NewsState.Error(e.message ?: "An error occurred")
            }
        }
    }
}

sealed class NewsState {
    object Loading : NewsState()
    knowledge class Success(val information: NewsItem) : NewsState()
    knowledge class Error(val error: String) : NewsState()
}

6. Set Up Navigation

Change Fragment-based navigation with Compose navigation:

class MainActivity : ComponentActivity() {
    override enjoyable onCreate(savedInstanceState: Bundle?) {
        tremendous.onCreate(savedInstanceState)
        setContent {
            NewsApp()
        }
    }
}

@Composable
enjoyable NewsApp() {
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "newsList") {
        composable("newsList") {
            val viewModel: NewsListViewModel = viewModel()
            NewsListScreen(viewModel = viewModel) { newsItem ->
                navController.navigate("newsDetail/${newsItem.id}")
            }
        }
        composable("newsDetail/{newsId}") { backStackEntry ->
            val viewModel: NewsDetailViewModel = viewModel()
            val newsId = backStackEntry.arguments?.getString("newsId")?.toIntOrNull() ?: 0
            viewModel.loadNewsDetail(newsId)
            NewsDetailScreen(viewModel = viewModel)
        }
    }
}

7. Take a look at and Iterate

After migrating the screens, totally take a look at the app to make sure it behaves as anticipated. Use Compose’s preview performance to visualise your UI:

@Preview(showBackground = true)
@Composable
enjoyable PreviewNewsListScreen() {
    NewsListScreen(viewModel = NewsListViewModel(), onItemClick = {})
}

@Preview(showBackground = true)
@Composable
enjoyable PreviewNewsDetailScreen() {
    NewsDetailScreen(viewModel = NewsDetailViewModel())
}

8. Progressively Migrate the Whole App

When you’re comfy with the migration course of, proceed migrating the remainder of your app incrementally. Use ComposeView and AndroidView to combine Compose with current XML





Supply hyperlink

Leave a Comment