Un guide étape par étape pour les développeurs / blogs Android / Perficient

Jetpack Compose est la boîte à outils moderne d’Android pour la construction de l’interface utilisateur native. Il simplifie et accélère le développement de l’interface utilisateur en utilisant une approche déclarative, qui est un changement significatif par rapport aux dispositions traditionnelles basées sur XML. Si vous avez une application Android existante écrite en Kotlin en utilisant le modèle MVP (modèle-View-Presenter) avec des dispositions XML, des fragments et des activités, la migration vers la composition de Jetpack peut apporter de nombreux avantages, notamment une amélioration de la productivité des développeurs, du code réduit de la chauffeur et d’un Architecture d’interface utilisateur plus moderne.
Dans cet article, nous allons parcourir les étapes pour migrer une application Android de MVP avec des dispositions XML à Jetpack Compose. Nous utiliserons une application de base de base pour expliquer en détail comment migrer toutes les couches de l’application. L’application a deux écrans:
- Un fragment de liste de nouvelles pour afficher une liste de nouvelles.
- Un fragment de détail des informations pour montrer les détails d’un article sélectionné.
Nous allons commencer par afficher l’implémentation MVP d’origine, y compris les présentateurs, puis migrer l’application vers Jetpack Compose étape par étape. Nous ajouterons également la gestion des erreurs, le chargement des états et utiliserons le flux de Kotlin au lieu de LiveData pour une approche plus moderne et réactive.
1. Comprendre les principales différences
Avant de plonger dans la migration, il est essentiel de comprendre les principales différences entre les deux approches:
- Impératif vs UI déclaratif: Les dispositions XML sont impératives, ce qui signifie que vous définissez la structure de l’interface utilisateur, puis la manipulez par programme. Jetpack Compose est déclaratif, ce qui signifie que vous décrivez à quoi devrait ressembler l’interface utilisateur pour un état donné et que Compose gère le rendu.
- MVP vs architecture de composition: MVP sépare la logique de l’interface utilisateur en présentateurs et vues. Jetpack Compose encourage une architecture plus réactive et axée sur l’État, utilisant souvent ViewModel et Hoisting d’État.
- Fragments et activités: Dans le développement traditionnel Android, les fragments et les activités sont utilisés pour gérer les composants de l’interface utilisateur. Dans Jetpack Compose, vous pouvez remplacer la plupart des fragments et activités par des fonctions composables.
2. Planifiez la migration
La migration d’une application entière vers la composition de Jetpack peut être une entreprise importante. Voici une approche suggérée:
- Commencer petit: Commencez par migrer un seul écran ou composant vers la composition de Jetpack. Cela vous aidera à comprendre le processus et à identifier les défis potentiels.
- Migration incrémentielle: Jetpack Compose est conçu pour fonctionner aux côtés des vues traditionnelles, afin que vous puissiez migrer progressivement votre application. Utiliser
ComposeView
dans les dispositions XML ouAndroidView
en composi pour combler l’écart. - MVP de refactor à MVVM: Jetpack Compose fonctionne bien avec le modèle MVVM (modèle-View-ViewModel). Envisagez de refactoriser vos présentateurs dans les modèles de vue.
- Remplacer les fragments par des fonctions composables: Les fragments peuvent être remplacés par des fonctions composables, simplifiant la navigation et la gestion de l’interface utilisateur.
- Ajouter des états de gestion des erreurs et de chargement: Assurez-vous que votre application gère gracieusement les erreurs et affiche les états de chargement pendant la récupération des données.
- Utilisez Kotlin Flow: Remplacez LiveData par le flux de Kotlin pour une approche plus moderne et réactive.
3. Configurer Jetpack Compose
Avant de commencer la migration, assurez-vous que votre projet est configuré pour la composition de Jetpack:
- Mettre à jour les dépendances Gradle:
Ajoutez les dépendances de composition nécessaires à votrebuild.gradle
déposer:android { ... buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion '1.5.3' } } dependencies { implementation 'androidx.activity:activity-compose:1.8.0' implementation 'androidx.compose.ui:ui:1.5.4' implementation 'androidx.compose.material:material: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 Flow implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' // For Flow }
- Activer la composition dans votre projet:
Assurez-vous que votre projet utilise les versions du plugin Kotlin et Android Gradle.
4. Implémentation MVP originale
un. Fragment de la liste des nouvelles et présentateur
Le NewsListFragment
Affiche une liste de nouvelles. Le NewsListPresenter
Repose les données et met à jour la vue.
NewslistFragment.kt
class NewsListFragment : Fragment(), NewsListView {
private lateinit var presenter: NewsListPresenter
private lateinit var adapter: NewsListAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_news_list, container, false)
val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerView)
adapter = NewsListAdapter { newsItem -> presenter.onNewsItemClicked(newsItem) }
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(context)
presenter = NewsListPresenter(this)
presenter.loadNews()
return view
}
override fun showNews(news: List<NewsItem>) {
adapter.submitList(news)
}
override fun showLoading() {
// Show loading indicator
}
override fun showError(error: String) {
// Show error message
}
}
NewsListPresenter.kt
class NewsListPresenter(private val view: NewsListView) {
fun loadNews() {
view.showLoading()
// Simulate fetching news from a data source (e.g., API or local database)
try {
val newsList = listOf(
NewsItem(id = 1, title = "News 1", summary = "Summary 1"),
NewsItem(id = 2, title = "News 2", summary = "Summary 2")
)
view.showNews(newsList)
} catch (e: Exception) {
view.showError(e.message ?: "An error occurred")
}
}
fun onNewsItemClicked(newsItem: NewsItem) {
// Navigate to the news detail screen
val intent = Intent(context, NewsDetailActivity::class.java).apply {
putExtra("newsId", newsItem.id)
}
startActivity(intent)
}
}
NewslistView.kt
interface NewsListView {
fun showNews(news: List<NewsItem>)
fun showLoading()
fun showError(error: String)
}
né Fragment de détails et présentateur
Le NewsDetailFragment
Affiche les détails d’un article sélectionné. Le NewsDetailPresenter
Repose les détails et met à jour la vue.
Newsdetailfragment.kt
class NewsDetailFragment : Fragment(), NewsDetailView {
private lateinit var presenter: NewsDetailPresenter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_news_detail, container, false)
presenter = NewsDetailPresenter(this)
val newsId = arguments?.getInt("newsId") ?: 0
presenter.loadNewsDetail(newsId)
return view
}
override fun showNewsDetail(newsItem: NewsItem) {
view?.findViewById<TextView>(R.id.title)?.text = newsItem.title
view?.findViewById<TextView>(R.id.summary)?.text = newsItem.summary
}
override fun showLoading() {
// Show loading indicator
}
override fun showError(error: String) {
// Show error message
}
}
Newsdetailpresenter.kt
class NewsDetailPresenter(private val view: NewsDetailView) {
fun loadNewsDetail(newsId: Int) {
view.showLoading()
// Simulate fetching news detail from a data source (e.g., API or local database)
try {
val newsItem = NewsItem(id = newsId, title = "News $newsId", summary = "Summary $newsId")
view.showNewsDetail(newsItem)
} catch (e: Exception) {
view.showError(e.message ?: "An error occurred")
}
}
}
NewsdetailView.kt
interface NewsDetailView {
fun showNewsDetail(newsItem: NewsItem)
fun showLoading()
fun showError(error: String)
}
5. Migrer vers Jetpack Compose
un. Migrer le fragment de liste de nouvelles
Remplacer le NewsListFragment
avec une fonction composable. Le NewsListPresenter
sera refactorisé dans un NewsListViewModel
.
NewslistScreen.kt
@Composable
fun NewsListScreen(viewModel: NewsListViewModel, onItemClick: (NewsItem) -> Unit) {
val newsState by viewModel.newsState.collectAsState()
when (newsState) {
is NewsState.Loading -> {
// Show loading indicator
CircularProgressIndicator()
}
is NewsState.Success -> {
val news = (newsState as NewsState.Success).news
LazyColumn {
items(news) { newsItem ->
NewsListItem(newsItem = newsItem, onClick = { onItemClick(newsItem) })
}
}
}
is NewsState.Error -> {
// Show error message
val error = (newsState as NewsState.Error).error
Text(text = error, color = Color.Red)
}
}
}
@Composable
fun NewsListItem(newsItem: NewsItem, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable { onClick() }
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = newsItem.title, style = MaterialTheme.typography.h6)
Text(text = newsItem.summary, style = MaterialTheme.typography.body1)
}
}
}
NewslistViewModel.kt
class NewsListViewModel : ViewModel() {
private val _newsState = MutableStateFlow<NewsState>(NewsState.Loading)
val newsState: StateFlow<NewsState> get() = _newsState
init {
loadNews()
}
private fun loadNews() {
viewModelScope.launch {
_newsState.value = NewsState.Loading
try {
// Simulate fetching news from a data source (e.g., API or local database)
val newsList = listOf(
NewsItem(id = 1, title = "News 1", summary = "Summary 1"),
NewsItem(id = 2, title = "News 2", summary = "Summary 2")
)
_newsState.value = NewsState.Success(newsList)
} catch (e: Exception) {
_newsState.value = NewsState.Error(e.message ?: "An error occurred")
}
}
}
}
sealed class NewsState {
object Loading : NewsState()
data class Success(val news: List<NewsItem>) : NewsState()
data class Error(val error: String) : NewsState()
}
né Migrer le fragment de détail des nouvelles
Remplacer le NewsDetailFragment
avec une fonction composable. Le NewsDetailPresenter
sera refactorisé dans un NewsDetailViewModel
.
NewsdetailScreen.kt
@Composable
fun NewsDetailScreen(viewModel: NewsDetailViewModel) {
val newsState by viewModel.newsState.collectAsState()
when (newsState) {
is NewsState.Loading -> {
// Show loading indicator
CircularProgressIndicator()
}
is NewsState.Success -> {
val newsItem = (newsState as NewsState.Success).news
Column(modifier = Modifier.padding(16.dp)) {
Text(text = newsItem.title, style = MaterialTheme.typography.h4)
Text(text = newsItem.summary, style = MaterialTheme.typography.body1)
}
}
is NewsState.Error -> {
// Show error message
val error = (newsState as NewsState.Error).error
Text(text = error, color = Color.Red)
}
}
}
NewsdetailViewModel.kt
class NewsDetailViewModel : ViewModel() {
private val _newsState = MutableStateFlow<NewsState>(NewsState.Loading)
val newsState: StateFlow<NewsState> get() = _newsState
fun loadNewsDetail(newsId: Int) {
viewModelScope.launch {
_newsState.value = NewsState.Loading
try {
// Simulate fetching news detail from a data source (e.g., API or local database)
val newsItem = NewsItem(id = newsId, title = "News $newsId", summary = "Summary $newsId")
_newsState.value = NewsState.Success(newsItem)
} catch (e: Exception) {
_newsState.value = NewsState.Error(e.message ?: "An error occurred")
}
}
}
}
sealed class NewsState {
object Loading : NewsState()
data class Success(val news: NewsItem) : NewsState()
data class Error(val error: String) : NewsState()
}
6. Configurer la navigation
Remplacez la navigation basée sur les fragments par la navigation de composition:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NewsApp()
}
}
}
@Composable
fun 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. Tester et itérer
Après avoir migré les écrans, testez soigneusement l’application pour s’assurer qu’elle se comporte comme prévu. Utilisez les fonctionnalités d’aperçu de Compose pour visualiser votre interface utilisateur:
@Preview(showBackground = true)
@Composable
fun PreviewNewsListScreen() {
NewsListScreen(viewModel = NewsListViewModel(), onItemClick = {})
}
@Preview(showBackground = true)
@Composable
fun PreviewNewsDetailScreen() {
NewsDetailScreen(viewModel = NewsDetailViewModel())
}
8. migrer progressivement l’ensemble de l’application
Une fois que vous êtes à l’aise avec le processus de migration, continuez à migrer progressivement le reste de votre application. Utiliser ComposeView
et AndroidView
Pour intégrer Compose avec XML existant
Source link