Se ha denunciado esta presentación.
Se está descargando tu SlideShare. ×

Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023

Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Próximo SlideShare
Stmik bandung
Stmik bandung
Cargando en…3
×

Eche un vistazo a continuación

1 de 55 Anuncio

Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023

Descargar para leer sin conexión

À l'automne dernier, nous avons eu la chance de développer une nouvelle app pour un de nos clients en partant de zéro.
L'objectif ? Créer une application minimale à mettre entre les mains de dizaines de beta testeurs, en 8 semaines et avec 2 développeurs. Partant d'une feuille blanche, nous avons pu mettre en œuvre les dernières avancées de la stack Android sans être contraints par l'existant.
Développeurs débutants comme expérimentés, vous repartirez de ce talk avec nos apprentissages clés sur l'architecture ainsi que sur les bibliothèques et astuces pour faciliter la maintenance et la stabilité de l'application. En bonus, nous répondrons à la question : "Une app full-compose, est-ce que c'est cool ?"

À l'automne dernier, nous avons eu la chance de développer une nouvelle app pour un de nos clients en partant de zéro.
L'objectif ? Créer une application minimale à mettre entre les mains de dizaines de beta testeurs, en 8 semaines et avec 2 développeurs. Partant d'une feuille blanche, nous avons pu mettre en œuvre les dernières avancées de la stack Android sans être contraints par l'existant.
Développeurs débutants comme expérimentés, vous repartirez de ce talk avec nos apprentissages clés sur l'architecture ainsi que sur les bibliothèques et astuces pour faciliter la maintenance et la stabilité de l'application. En bonus, nous répondrons à la question : "Une app full-compose, est-ce que c'est cool ?"

Anuncio
Anuncio

Más Contenido Relacionado

Similares a Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023 (20)

Más reciente (20)

Anuncio

Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023

  1. 1. Nicolas HAAN - 24/01/2023 Comment développer une application Android en 8 semaines ?
  2. 2. BAM en quelques mots • 120 experts du développement mobile • 3 domaines d’expertise (Développement, Produit et design) • + 150 projets (UEFA, TF1, URGO, Pass Culture) • 2 bureaux en France (Paris et Nantes)
  3. 3. M33
  4. 4. La tribe native de BAM
  5. 5. The project
  6. 6. The project • 2 developers, 8 weeks • A finished MVP, not a POC • State of the art technical stack, architecture and testing strategy • A code base ready to taken over • A new app from scratch
  7. 7. Architecture Gradle Modularisation Dependency management Unit Testing UI layer
  8. 8. Architecture? Let's go clean!
  9. 9. Clean Architecture
  10. 10. Google Architecture guidelines
  11. 11. Google Architecture guidelines
  12. 12. Google Architecture guidelines • Separation of concerns • Drive UI from data models • Single source of truth • Unidirectional Data Flow
  13. 13. Modular architecture
  14. 14. FeatureScreen FeatureViewModel FeatureUseCase FeatureRepository FeatureDataSource ApiService DATA DOMAIN UI
  15. 15. FeatureScreen FeatureViewModel FeatureUseCase FeatureRepository FeatureDataSource ApiService Transformer le state exposé par le VM en vues affichables, récupérer les actions utilisateur aggrégation des données des UseCase pour construire le state de l'écran, récupérer les évènements de la Vue et les dispatcher vers les UseCases Implémentation des règles métiers, agrégations des sources de données, abstraction des repositories Single Source of Truth de la donnée, règles métiers du cache et de la persistence abstraction de l'implémentation de la datasource, préparation des paramètres des WS, mapping Dto <-> Domain appel réseau, parsing des JSON, remontée des erreurs
  16. 16. Architecture • Strict separation of concern • Heavy modularisation (~22 modules for 5 screens) • Confident that it will scale well and can be taken over
  17. 17. Gradle: Beyond Android Studio Template
  18. 18. Groovy or KTS? KTS promises • Available since Android Gradle Plugin 4.1.0 (August 2020) • The Kotlin syntax we all know and love available for Gradle! • Autocompletion and type safety in Gradle • The future of Gradle configuration files
  19. 19. Groovy or KTS? • In 2022, templates are still using Groovy, even in Flamingo • Manual migration • Autocompletion not really useful in day to day use • Not all libraries and open source projects have adopted it
  20. 20. 🤷
  21. 21. Modularisation: Convention plugins
  22. 22. Sharing build logic "Historical method" subprojects { tasks.withType(Test::class.java) { // ... } } allprojects { repositories { // .. } apply(plugin = "jacoco") jacoco { toolVersion = "0.8.7" } } • Using subprojects and allprojects closures • Not very flexible • Not explicit from a module point of view
  23. 23. Sharing build logic modular gradle files apply from: "$rootDir/dependencies.gradle" apply from: "$rootDir/gradle/baseFeature.gradle" apply from: "$rootDir/gradle/flavors.gradle" • doesn't scale well • Doesn't work well with KTS (see here and here)
  24. 24. Sharing build logic Convention plugin • create convention plugins to be applied to relevant modules • There can be several modular and composable plugins • Plugins are explicitly applied to each module buildSrc androidBase.kts flavors.kts core/moduleX ... domain/moduleY
  25. 25. Sharing build logic Convention plugin plugins { id("com.android.library") id("kotlin-android") id("kotlin-kapt") id("org.jlleitschuh.gradle.ktlint") } android { compileSdk = findIntProperty("compileSdkVersion") defaultConfig { minSdk = findIntProperty("minSdkVersion") targetSdk = findIntProperty("targetSdkVersion") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 }
  26. 26. Sharing build logic Convention plugin plugins { id("com.android.library") id("kotlin-android") id("kotlin-kapt") id("org.jlleitschuh.gradle.ktlint") } android { compileSdk = findIntProperty("compileSdkVersion") defaultConfig { minSdk = findIntProperty("minSdkVersion") targetSdk = findIntProperty("targetSdkVersion") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } buildTypes { release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } testOptions { unitTests.isReturnDefaultValues = true unitTests.all { it.testLogging { events = setOf( TestLogEvent.STARTED, TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED ) showStandardStreams = true } } } flavorDimensions.add("environment") productFlavors { register("dev") { dimension = "environment" } register("staging") { dimension = "environment" } register("prod") { dimension = "environment" } } } dependencies { implementation(Koin.android) implementation(Koin.compose) } fun findStringProperty(propertyName: String): String { return findProperty(propertyName) as String } fun findIntProperty(propertyName: String): Int { return (findProperty(propertyName) as String).toInt() } plugins { id("com.client.app.convention.androidBase") id("com.client.app.convention.flavors") } android { namespace = "com.client.app.logging" } dependencies { implementation(libs.appcenter.crashes) } moduleX/build.gradle.kts
  27. 27. Sharing build logic Convention plugin plugins { id("com.android.library") id("kotlin-android") id("kotlin-kapt") id("org.jlleitschuh.gradle.ktlint") } android { compileSdk = findIntProperty("compileSdkVersion") defaultConfig { minSdk = findIntProperty("minSdkVersion") targetSdk = findIntProperty("targetSdkVersion") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } buildTypes { release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } testOptions { unitTests.isReturnDefaultValues = true unitTests.all { it.testLogging { events = setOf( TestLogEvent.STARTED, TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED ) showStandardStreams = true } } } flavorDimensions.add("environment") productFlavors { register("dev") { dimension = "environment" } register("staging") { dimension = "environment" } register("prod") { dimension = "environment" } } } dependencies { implementation(Koin.android) implementation(Koin.compose) } fun findStringProperty(propertyName: String): String { return findProperty(propertyName) as String } fun findIntProperty(propertyName: String): Int { return (findProperty(propertyName) as String).toInt() } plugins { id("com.client.app.convention.androidBase") id("com.client.app.convention.flavors") } android { namespace = "com.client.app.logging" } dependencies { implementation(libs.appcenter.crashes) } moduleX/build.gradle.kts
  28. 28. Sharing build logic Convention plugin plugins { id("com.android.library") id("kotlin-android") id("kotlin-kapt") id("org.jlleitschuh.gradle.ktlint") } android { compileSdk = findIntProperty("compileSdkVersion") defaultConfig { minSdk = findIntProperty("minSdkVersion") targetSdk = findIntProperty("targetSdkVersion") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } buildTypes { release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } testOptions { unitTests.isReturnDefaultValues = true unitTests.all { it.testLogging { events = setOf( TestLogEvent.STARTED, TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED ) showStandardStreams = true } } } flavorDimensions.add("environment") productFlavors { register("dev") { dimension = "environment" } register("staging") { dimension = "environment" } register("prod") { dimension = "environment" } } } dependencies { implementation(Koin.android) implementation(Koin.compose) } fun findStringProperty(propertyName: String): String { return findProperty(propertyName) as String } fun findIntProperty(propertyName: String): Int { return (findProperty(propertyName) as String).toInt() } plugins { id("com.client.app.convention.androidBase") id("com.client.app.convention.flavors") } android { namespace = "com.client.app.logging" } dependencies { implementation(libs.appcenter.crashes) } moduleX/build.gradle.kts
  29. 29. Sharing build logic Convention plugin plugins { id("com.android.library") id("kotlin-android") id("kotlin-kapt") id("org.jlleitschuh.gradle.ktlint") } android { compileSdk = findIntProperty("compileSdkVersion") defaultConfig { minSdk = findIntProperty("minSdkVersion") targetSdk = findIntProperty("targetSdkVersion") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } buildTypes { release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } testOptions { unitTests.isReturnDefaultValues = true unitTests.all { it.testLogging { events = setOf( TestLogEvent.STARTED, TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED ) showStandardStreams = true } } } flavorDimensions.add("environment") productFlavors { register("dev") { dimension = "environment" } register("staging") { dimension = "environment" } register("prod") { dimension = "environment" } } } dependencies { implementation(Koin.android) implementation(Koin.compose) } fun findStringProperty(propertyName: String): String { return findProperty(propertyName) as String } fun findIntProperty(propertyName: String): Int { return (findProperty(propertyName) as String).toInt() } moduleX/build.gradle.kts plugins { id("com.client.app.convention.androidBase") id("com.client.app.convention.flavors") } android { namespace = "com.client.app.logging" } dependencies { implementation(libs.appcenter.crashes) }
  30. 30. Sharing build logic Next step: explicit convention plugin class AndroidLibraryJacocoConventionPlugin : Plugin<Project> { override fun apply(target: Project) { with(target) { with(pluginManager) { apply("org.gradle.jacoco") apply("com.android.library") } val extension = extensions.getByType<LibraryAndroidComponentsExtension>() configureJacoco(extension) } } }
  31. 31. Dependencies: refreshVersions library
  32. 32. Dependencies dependencies { implementation 'androidx.core:core-ktx:1.9.0' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material3:material3:1.0.1" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' implementation 'androidx.activity:activity-compose:1.6.1' implementation "com.google.accompanist:accompanist-webview:0.28.0" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" }
  33. 33. Dependencies
  34. 34. Dependencies Custom dependency management • No built-in mechanism to centralise dependencies* • Custom dependencies management breaks Android Studio dependencies update
  35. 35. Dependencies refreshVersions library • Centralize dependencies declarations and versions in versions.properties • Helpful gradle task for version updates and even migration!
  36. 36. Dependencies refreshVersions library ./gradlew refreshVersions
  37. 37. Dependencies refreshVersions library: migration dependencies { val composeVersion: String = project.properties["composeVersion"] as String implementation("androidx.core:core-ktx:1.7.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1") implementation("androidx.activity:activity-compose:1.3.1") implementation("androidx.compose.ui:ui:$composeVersion") implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion") implementation("androidx.compose.material3:material3:1.0.0-alpha02") implementation("net.openid:appauth:0.11.1") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.3") androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") androidTestImplementation("androidx.compose.ui:ui-test-junit4:$composeVersion") debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion") debugImplementation("androidx.compose.ui:ui-test-manifest:$composeVersion") } ./gradlew refreshVersionsMigrate
  38. 38. Dependencies refreshVersions library: migration dependencies { implementation(AndroidX.core.ktx) implementation(AndroidX.lifecycle.runtime.ktx) implementation(AndroidX.activity.compose) implementation(AndroidX.compose.ui) implementation(AndroidX.compose.ui.toolingPreview) implementation(AndroidX.compose.material3) implementation("net.openid:appauth:_") testImplementation(Testing.junit4) androidTestImplementation(AndroidX.test.ext.junit) androidTestImplementation(AndroidX.test.espresso.core) androidTestImplementation(AndroidX.compose.ui.testJunit4) debugImplementation(AndroidX.compose.ui.tooling) debugImplementation(AndroidX.compose.ui.testManifest) }
  39. 39. RefreshVersions Other features • Support gradle plugins versions as well • RefreshVersionsBot (~Dependabot) • Support for Gradle VersionCatalog • Migration still works after initial migration! dependencies { implementation(AndroidX.compose.ui) implementation(AndroidX.compose.ui.toolingPreview) implementation(AndroidX.compose.material3) implementation("com.google.accompanist:accompanist-webview:0.28.0") }
  40. 40. RefreshVersions Other features • Support gradle plugins versions as well • RefreshVersionsBot (~Dependabot) • Support for Gradle VersionCatalog • Migration still works after initial migration! dependencies { implementation(AndroidX.compose.ui) implementation(AndroidX.compose.ui.toolingPreview) implementation(AndroidX.compose.material3) implementation(libs.accompanist.webview) }
  41. 41. New Gradle features and trends since 3~4 years • Kotlin script (KTS) • Convention plugins • Convention plugins (explicit) • pluginManagement { } block • dependencyResolutionManagement { } block • Version catalog + modularisation + third party libraries 🤯
  42. 42. Unit Testing: the Outside-In strategy
  43. 43. Unit testing Common issues • fragile tests • many test doubles ("mocks") to implement • testing private methods • testing behaviours (implementations) • Unit tests different from acceptance tests • Should I test pass-through classes?
  44. 44. Unit testing Common issues FeatureScreen FeatureViewModel FeatureUseCase FeatureRepository FeatureDataSource ApiService Backend
  45. 45. Outside-In Strategy Testing each class in isolation FeatureScreen FeatureViewModel FeatureUseCase FeatureRepository FeatureDataSource ApiService MockWebServer Unit tests
  46. 46. mocked JSON files
  47. 47. @Test fun `Application ToBeCompleted has a correct UI header`() = runTest { val contractReference = "CFR20221025MHP37ZZ" val validJson = readMockFile("mocked-responses/api/v1/Applications/$contractReference/details") restartKoinWithMockedWebServer( responses = listOf(validJson), featureModule = featureModule, testKoinModule = testKoinModule, ) val viewModel: ApplicationDetailViewModel = get(parameters = { parametersOf(contractReference) }) viewModel.onAction(ApplicationDetailScreenAction.OnResume) viewModel.uiState.test { val details = awaitForItemIs(ApplicationDetailScreenState.Details::class) assertEquals(ToBeCompleted, details.finalState) } } // GIVEN // WHEN // THEN // THEN mockWebServer Koin Turbine
  48. 48. Generate the network model: Using OpenAPI generator
  49. 49. OpenAPI • The specification used by Swagger tools • Describes the API (endpoints, objects...) • Can be used as a contract between the backend and the frontend • Can be used to generate the code of the client and/or the server
  50. 50. spec.json OpenAPI Generator *Api.kt *Dto.kt
  51. 51. OpenAPI Generator spec.json OpenAPI Generator /** * Tweet delete by Tweet ID * Delete specified Tweet (in the path) by ID. * @param id The ID of the Tweet to be deleted. * @return TweetDeleteResponse * ... */ fun deleteTweetById(id: kotlin.String) : TweetDeleteResponse data class Tweet ( /* Unique identifier of this Tweet. This is returned as a string in order to avoid complications with languages and tools that cannot handle large integers. */ @Json(name = "id") val id: kotlin.String, /* The content of the Tweet. */ @Json(name = "text") val text: kotlin.String, /* Unique identifier of this User.*/ @Json(name = "author_id") val authorId: kotlin.String? = null, // ...
  52. 52. OpenAPI Generator • Allows to integrate BFF updates in a blink of an eye • Quite flexible to integrate • see more: https://www.bam.tech/article/customize-openapi-generator-output-for- your-android-app
  53. 53. UI layer: full Compose!
  54. 54. Jetpack Compose • Really easy to implement the client Design System • Works really well with MVI approach • Webviews are buggy • Jetpack Compose navigation s*cks!
  55. 55. Thank you!

×