
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:
- A Information Listing Fragment to show a listing of reports objects.
- 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:
- Begin Small: Start by migrating a single display or part to Jetpack Compose. This may assist you perceive the method and establish potential challenges.
- Incremental Migration: Jetpack Compose is designed to work alongside conventional Views, so you possibly can migrate your app incrementally. Use
ComposeView
in XML layouts orAndroidView
in Compose to bridge the hole. - Refactor MVP to MVVM: Jetpack Compose works effectively with the MVVM (Mannequin-View-ViewModel) sample. Contemplate refactoring your Presenters into ViewModels.
- Change Fragments with Composable Capabilities: Fragments may be changed with composable features, simplifying navigation and UI administration.
- Add Error Dealing with and Loading States: Guarantee your app handles errors gracefully and shows loading states throughout knowledge fetching.
- 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:
- Replace Gradle Dependencies:
Add the mandatory Compose dependencies to yourconstruct.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 }
- 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