From 621ff0fea25ff48d80b1067d39df99d001eb9d11 Mon Sep 17 00:00:00 2001 From: yudha-haris Date: Sat, 11 Jan 2025 22:54:31 +0700 Subject: [PATCH 01/10] chore: update to gradle 8.3 and dependencies support for flutter 3.27 --- android/app/build.gradle | 66 +-- android/build.gradle | 17 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 30 +- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + pubspec.lock | 483 +++++++++++------- pubspec.yaml | 19 +- 8 files changed, 352 insertions(+), 267 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 1e11b2f..dfae5d2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,72 +1,44 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - namespace "com.example.boilerplate" - compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion + namespace = "com.example.boilerplate" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + jvmTarget = JavaVersion.VERSION_17 } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.boilerplate" + applicationId = "com.example.boilerplate" // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig = signingConfigs.debug } } } flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + source = "../.." } diff --git a/android/build.gradle b/android/build.gradle index f7eb7f6..d2ffbff 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -18,12 +5,12 @@ allprojects { } } -rootProject.buildDir = '../build' +rootProject.buildDir = "../build" subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { - project.evaluationDependsOn(':app') + project.evaluationDependsOn(":app") } tasks.register("clean", Delete) { diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 3c472b9..7bb2df6 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 44e62bc..a42444d 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.2.1" apply false + id "org.jetbrains.kotlin.android" version "1.8.22" apply false +} + +include ":app" diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/pubspec.lock b/pubspec.lock index ad02e7b..92e6304 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,31 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "76.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.11.0" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.6.0" async: dependency: transitive description: @@ -37,18 +42,18 @@ packages: dependency: transitive description: name: bloc - sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "8.1.4" bloc_test: dependency: "direct main" description: name: bloc_test - sha256: "55a48f69e0d480717067c5377c8485a3fcd41f1701a820deef72fa0f4ee7215f" + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" url: "https://pub.dev" source: hosted - version: "9.1.6" + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -61,50 +66,50 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.3" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.14" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "8.0.0" built_collection: dependency: transitive description: @@ -117,10 +122,10 @@ packages: dependency: transitive description: name: built_value - sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" url: "https://pub.dev" source: hosted - version: "8.9.1" + version: "8.9.3" characters: dependency: transitive description: @@ -149,58 +154,58 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" coverage: dependency: transitive description: name: coverage - sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" + sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 url: "https://pub.dev" source: hosted - version: "1.7.2" + version: "1.11.1" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.7" dartz: dependency: "direct main" description: @@ -221,34 +226,42 @@ packages: dependency: "direct main" description: name: dio - sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8" + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" url: "https://pub.dev" source: hosted - version: "5.4.1" + version: "2.0.0" envied: dependency: "direct main" description: name: envied - sha256: dab29e21452c3d57ec10889d96b06b4a006b01375d4df10b33c9704800c208c4 + sha256: "129a0dbf32b90344fa2e9d6943569fdec8f17904e66161e0a1f09ee3416508ae" url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "1.0.0" envied_generator: dependency: "direct dev" description: name: envied_generator - sha256: b8655d5cb39b4d1d449a79ff6f1367b252c23955ff17ec7c03aacdff938598bd + sha256: "76aec98907872ce8488f021e68d213bd0d9bf224eb393a094be1708cc3180d41" url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "1.0.0" equatable: dependency: "direct main" description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" fake_async: dependency: transitive description: @@ -261,26 +274,26 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -290,74 +303,82 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: "87325da1ac757fcc4813e6b34ed5dd61169973871fdf181d6c2109dd6935ece1" + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "8.1.6" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "5.0.0" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage - sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685 + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "9.2.4" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" + sha256: bf7404619d7ab5c0a1151d7c4e802edad8f33535abfbeff2f9e1fe1274e2d705 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.3" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.2" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.1" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108" + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_staggered_grid_view: + dependency: transitive + description: + name: flutter_staggered_grid_view + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "0.7.0" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b url: "https://pub.dev" source: hosted - version: "2.0.10+1" + version: "2.0.17" flutter_test: dependency: "direct dev" description: flutter @@ -372,34 +393,34 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5" + sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.5.7" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" get_it: dependency: "direct main" description: name: get_it - sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 + sha256: f126a3e286b7f5b578bf436d5592968706c4c1de28a228b870ce375d9f743103 url: "https://pub.dev" source: hosted - version: "7.6.7" + version: "8.0.3" glob: dependency: transitive description: @@ -412,66 +433,66 @@ packages: dependency: "direct main" description: name: go_router - sha256: e1a30a66d734f9e498b1b6522d6a75ded28242bad2359a9158df38a1c30bcf1f + sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d" url: "https://pub.dev" source: hosted - version: "10.2.0" + version: "14.6.3" google_fonts: dependency: "direct main" description: name: google_fonts - sha256: e20ff62b158b96f392bfc8afe29dee1503c94fbea2cbe8186fd59b756b8ae982 + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "6.2.1" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" http: dependency: transitive description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" infinite_scroll_pagination: dependency: "direct main" description: name: infinite_scroll_pagination - sha256: "9517328f4e373f08f57dbb11c5aac5b05554142024d6b60c903f3b73476d52db" + sha256: "4047eb8191e8b33573690922a9e995af64c3949dc87efc844f936b039ea279df" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.1.0" io: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -484,82 +505,114 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c + url: "https://pub.dev" + source: hosted + version: "6.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + url: "https://pub.dev" + source: hosted + version: "10.0.7" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "6.7.1" + version: "3.0.8" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "5.1.1" logger: dependency: "direct main" description: name: logger - sha256: "7ad7215c15420a102ec687bb320a7312afd449bac63bfb1c60d9787c27b9767f" + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "2.5.0" logging: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" matcher: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" mocktail: dependency: "direct main" description: name: mocktail - sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" nested: dependency: transitive description: @@ -580,50 +633,50 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: transitive description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -644,10 +697,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: @@ -660,10 +713,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -692,18 +745,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.5.0" recase: dependency: transitive description: @@ -716,10 +769,10 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_packages_handler: dependency: transitive description: @@ -732,31 +785,31 @@ packages: dependency: transitive description: name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.1" skeletonizer: dependency: "direct main" description: name: skeletonizer - sha256: "2eb80153c80507359ff05f6a18ed50ae0bafa1b999aa867a8cef0a53387b5650" + sha256: "0dcacc51c144af4edaf37672072156f49e47036becbc394d7c51850c5c1e884b" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.4.3" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" sliver_tools: dependency: transitive description: @@ -777,26 +830,26 @@ packages: dependency: transitive description: name: source_helper - sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "1.3.5" source_map_stack_trace: dependency: transitive description: name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" source_maps: dependency: transitive description: name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" url: "https://pub.dev" source: hosted - version: "0.10.12" + version: "0.10.13" source_span: dependency: transitive description: @@ -805,14 +858,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sqflite: + dependency: "direct main" + description: + name: sqflite + sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + url: "https://pub.dev" + source: hosted + version: "2.5.4+6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" + url: "https://pub.dev" + source: hosted + version: "2.4.1+1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -825,18 +918,26 @@ packages: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "3.3.0+3" term_glyph: dependency: transitive description: @@ -849,66 +950,66 @@ packages: dependency: transitive description: name: test - sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" url: "https://pub.dev" source: hosted - version: "1.24.9" + version: "1.25.8" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.3" test_core: dependency: transitive description: name: test_core - sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" url: "https://pub.dev" source: hosted - version: "0.5.9" + version: "0.6.5" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.15" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.16" vector_math: dependency: transitive description: @@ -921,34 +1022,42 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.3.0" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.1" webkit_inspection_protocol: dependency: transitive description: @@ -961,18 +1070,18 @@ packages: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.10.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -985,10 +1094,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.10.0" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 1a4436c..76d2aa5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,27 +41,28 @@ dependencies: dartz: ^0.10.1 dio: ^5.4.1 # E - envied: ^0.5.3 + envied: ^1.0.0 equatable: ^2.0.5 # F freezed_annotation: ^2.4.1 flutter_bloc: ^8.1.3 - flutter_secure_storage: ^9.0.0 + flutter_secure_storage: ^9.2.4 flutter_svg: ^2.0.10+1 # G - go_router: ^10.2.0 - get_it: ^7.6.0 - google_fonts: ^5.1.0 + go_router: ^14.6.3 + get_it: ^8.0.3 + google_fonts: ^6.2.1 # I - infinite_scroll_pagination: ^3.2.0 + infinite_scroll_pagination: ^4.1.0 # J json_annotation: ^4.8.1 # L - logger: ^1.4.0 + logger: ^2.5.0 # M mocktail: ^1.0.3 # S skeletonizer: ^1.1.0 + sqflite: ^2.4.1 dev_dependencies: flutter_test: @@ -69,14 +70,14 @@ dev_dependencies: build_runner: ^2.4.6 json_serializable: ^6.7.1 freezed: ^2.4.1 - envied_generator: ^0.5.3 + envied_generator: ^1.0.0 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^2.0.0 + flutter_lints: ^5.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From f81dab680c9c58be8fe79b9f60636ed56d76c888 Mon Sep 17 00:00:00 2001 From: yudha-haris Date: Sat, 11 Jan 2025 23:04:37 +0700 Subject: [PATCH 02/10] docs: update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 848d3e7..9c24d4b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 👨‍💻 Flutter Boilerplate -[![Generic badge](https://img.shields.io/badge/Flutter-v3.16.1-blue)](https://flutter.dev/docs) -[![Generic badge](https://img.shields.io/badge/Dart-v3.2.1-blue)](https://dart.dev/guides) +[![Generic badge](https://img.shields.io/badge/Flutter-v3.27.1-blue)](https://flutter.dev/docs) +[![Generic badge](https://img.shields.io/badge/Dart-v3.6.0-blue)](https://dart.dev/guides) Flutter Template From 5e5956bfd6210e8c43dcd2eea9ea28fcf2c31056 Mon Sep 17 00:00:00 2001 From: yudha-haris Date: Tue, 14 Jan 2025 06:22:59 +0700 Subject: [PATCH 03/10] refactor: implement injectable and change bloc structure according to official documentation --- ios/Podfile | 44 ++++++ lib/core/client/app_environment.dart | 19 +++ lib/core/client/network_exception.dart | 111 +++++++-------- lib/core/client/network_service.dart | 110 ++++++++------- lib/core/client/network_utils.dart | 2 + lib/core/constants/app_key.dart | 3 + lib/core/database/secure_database.dart | 2 + .../data/auth_repository_impl.dart | 8 +- .../data/local/auth_local_data_sources.dart | 4 +- .../data/remote/auth_remote_data_sources.dart | 6 +- .../model/mapper/auth_mapper.dart | 2 +- .../model/request/post_login_request.dart | 0 .../model/request/save_token_request.dart | 0 .../model/responses/auth_response.dart | 3 +- .../di/authentication_module.dart | 30 ---- .../domain/authentication_interactor.dart | 2 + .../blocs/authentication_bloc.dart | 6 +- .../presentation/pages/login_page.dart | 55 ++++---- .../data/onboarding_repository_impl.dart | 4 +- .../model/mapper/onboarding_mapper.dart | 3 +- .../model/responses/onboarding_response.dart | 0 .../onboarding_remote_data_sources.dart | 4 +- .../onboarding/di/onboarding_module.dart | 19 --- .../domain/authentication_interactor.dart | 2 + .../presentation/blocs/onboarding_bloc.dart | 2 + .../presentation/pages/onboarding_page.dart | 2 +- .../presentation/pages/splash_page.dart | 24 ++-- .../product/data/product_repository_impl.dart | 6 +- .../model/mapper/product_mapper.dart | 5 +- .../request/get_list_product_request.dart | 0 .../responses/list_product_response.dart | 2 +- .../responses/product_item_response.dart | 0 .../responses/product_user_response.dart | 0 .../remote/product_remote_data_sources.dart | 9 +- lib/features/product/di/product_module.dart | 19 --- .../product/domain/product_interactor.dart | 2 + .../home/blocs/product_home_bloc.dart | 10 +- .../local/profile_local_data_sources.dart | 2 + .../profile/data/profile_repository_impl.dart | 4 +- .../model/mapper/profile_mapper.dart | 3 +- .../model/responses/user_response.dart | 0 .../remote/profile_remote_data_sources.dart | 4 +- lib/features/profile/di/profile_module.dart | 23 ---- lib/main.dart | 5 +- lib/main_production.dart | 5 +- lib/services/di.config.dart | 128 ++++++++++++++++++ lib/services/di.dart | 70 ++-------- lib/services/secure_storage.dart | 2 + pubspec.lock | 16 +++ pubspec.yaml | 8 +- 50 files changed, 445 insertions(+), 345 deletions(-) create mode 100644 ios/Podfile create mode 100644 lib/core/client/app_environment.dart rename lib/features/authentication/data/{ => remote}/model/mapper/auth_mapper.dart (92%) rename lib/features/authentication/data/{ => remote}/model/request/post_login_request.dart (100%) rename lib/features/authentication/data/{ => remote}/model/request/save_token_request.dart (100%) rename lib/features/authentication/data/{ => remote}/model/responses/auth_response.dart (88%) delete mode 100644 lib/features/authentication/di/authentication_module.dart rename lib/features/onboarding/data/{ => remote}/model/mapper/onboarding_mapper.dart (82%) rename lib/features/onboarding/data/{ => remote}/model/responses/onboarding_response.dart (100%) delete mode 100644 lib/features/onboarding/di/onboarding_module.dart rename lib/features/product/data/{ => remote}/model/mapper/product_mapper.dart (80%) rename lib/features/product/data/{ => remote}/model/request/get_list_product_request.dart (100%) rename lib/features/product/data/{ => remote}/model/responses/list_product_response.dart (83%) rename lib/features/product/data/{ => remote}/model/responses/product_item_response.dart (100%) rename lib/features/product/data/{ => remote}/model/responses/product_user_response.dart (100%) delete mode 100644 lib/features/product/di/product_module.dart rename lib/features/profile/data/{ => remote}/model/mapper/profile_mapper.dart (84%) rename lib/features/profile/data/{ => remote}/model/responses/user_response.dart (100%) delete mode 100644 lib/features/profile/di/profile_module.dart create mode 100644 lib/services/di.config.dart diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/core/client/app_environment.dart b/lib/core/client/app_environment.dart new file mode 100644 index 0000000..49920a9 --- /dev/null +++ b/lib/core/client/app_environment.dart @@ -0,0 +1,19 @@ +import 'package:boilerplate/core/constants/app_key.dart'; +import 'package:boilerplate/core/constants/secrets.dart'; +import 'package:injectable/injectable.dart'; + +abstract class AppEnvironment { + String get baseUrl; +} + +@Injectable(env: [AppKey.devEnv], as: AppEnvironment) +class DevEnvironment implements AppEnvironment { + @override + String get baseUrl => Secret.baseUrlDev; +} + +@Injectable(env: [AppKey.prodEnv], as: AppEnvironment) +class ProdEnvironment implements AppEnvironment { + @override + String get baseUrl => Secret.baseUrlProd; +} \ No newline at end of file diff --git a/lib/core/client/network_exception.dart b/lib/core/client/network_exception.dart index f86fcee..fd8268e 100644 --- a/lib/core/client/network_exception.dart +++ b/lib/core/client/network_exception.dart @@ -31,9 +31,11 @@ class NetworkException implements Exception { case 408: return SendTimeOutException(); case 413: - return RequestEntityTooLargeException(response: response, message: message); + return RequestEntityTooLargeException( + response: response, message: message); case 422: - return UnprocessableEntityException(response: response, message: message); + return UnprocessableEntityException( + response: response, message: message); case 500: return InternalServerErrorException(); case 503: @@ -48,8 +50,8 @@ class NetworkException implements Exception { } static NetworkException handleException(Exception e) { - if (e is DioException){ - switch (e.type){ + if (e is DioException) { + switch (e.type) { case DioExceptionType.badResponse: final err = NetworkException.handleBadResponse(e.response); Logger().e(err.toString()); @@ -68,9 +70,9 @@ class NetworkException implements Exception { return FetchDataException(); } } - if(e is FormatException){ + if (e is FormatException) { Logger().e('Error: Format from front end error'); - } else if(e is SocketException){ + } else if (e is SocketException) { Logger().e('Error: No Internet Connection'); } return GeneralException(message: e.toString()); @@ -96,106 +98,89 @@ class InternalServerErrorException extends NetworkException { class ConflictException extends NetworkException { ConflictException({String? message, Response? response}) : super( - message: message, - prefix: 'Conflict', - response: response, - ); + message: message, + prefix: 'Conflict', + response: response, + ); } class RequestEntityTooLargeException extends NetworkException { RequestEntityTooLargeException({String? message, Response? response}) : super( - message: message, - prefix: 'Request Entity Too Large', - response: response, - ); + message: message, + prefix: 'Request Entity Too Large', + response: response, + ); } class FetchDataException extends NetworkException { FetchDataException({String? message, Response? response}) : super( - message: message, - prefix: 'Error During Communication', - response: response, - ); + message: message, + prefix: 'Error During Communication', + response: response, + ); } class NotFoundException extends NetworkException { NotFoundException({String? message, Response? response}) : super( - message: message, - prefix: 'Not Found', - response: response, - ); + message: message, + prefix: 'Not Found', + response: response, + ); } class UnprocessableEntityException extends NetworkException { UnprocessableEntityException({String? message, Response? response}) : super( - message: message, - prefix: 'Invalid Request', - response: response, - ); + message: message, + prefix: 'Invalid Request', + response: response, + ); String? getErrorMessage() { - return response?.data != null && response?.data['message'] != null ? response!.data['message'] : null; + return response?.data != null && response?.data['message'] != null + ? response!.data['message'] + : null; } } class BadRequestException extends NetworkException { BadRequestException({String? message, Response? response}) : super( - message: message, - prefix: 'Invalid Request', - response: response, - ); + message: message, + prefix: 'Invalid Request', + response: response, + ); String? getErrorMessage() { - return response?.data != null && response?.data['message'] != null ? response!.data['message'] : null; + return response?.data != null && response?.data['message'] != null + ? response!.data['message'] + : null; } } class UnauthorisedException extends NetworkException { - UnauthorisedException({String? message, Response? response}) - : super( - message: message, - prefix: 'Unauthorised', - response: response, - ); + UnauthorisedException({super.message, super.response}) + : super(prefix: 'Unauthorised'); } class InvalidInputException extends NetworkException { - InvalidInputException({String? message, Response? response}) - : super( - message: message, - prefix: 'Invalid Input', - response: response, - ); + InvalidInputException({super.message, super.response}) + : super(prefix: 'Invalid Input'); } class RequestCancelled extends NetworkException { - RequestCancelled({String? message, Response? response}) - : super( - message: message, - prefix: 'Request Cancelled', - response: response, - ); + RequestCancelled({super.message, super.response}) + : super(prefix: 'Request Cancelled'); } class BadCertificate extends NetworkException { - BadCertificate({String? message, Response? response}) - : super( - message: message, - prefix: 'BadCertificate', - response: response, - ); + BadCertificate({super.message, super.response}) + : super(prefix: 'BadCertificate'); } - class GeneralException extends NetworkException { - GeneralException({String? message}) - : super( - message: message, - prefix: 'General Exception', - ); -} \ No newline at end of file + GeneralException({super.message}) : super(prefix: 'General Exception'); +} diff --git a/lib/core/client/network_service.dart b/lib/core/client/network_service.dart index c5e58a8..51bc00f 100644 --- a/lib/core/client/network_service.dart +++ b/lib/core/client/network_service.dart @@ -1,66 +1,20 @@ import 'dart:convert'; +import 'package:boilerplate/core/client/app_environment.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; +import 'package:injectable/injectable.dart'; import 'package:logger/logger.dart'; import 'network_utils.dart'; -enum NetworkServiceType { - production, - staging, -} - -final interceptors = QueuedInterceptorsWrapper(onRequest: (options, handler) { - if (kDebugMode) { - Logger().i({ - 'api': options.path, - 'headers': options.headers, - 'queryParams': options.queryParameters, - }); - } - return handler.next(options); -}, onResponse: (resp, handler) { - if (kDebugMode) { - final isBytes = resp.requestOptions.responseType == ResponseType.bytes; - Logger().i({ - 'api': ''' -${resp.statusCode}: ${resp.requestOptions.baseUrl}${resp.requestOptions.path}''', - 'headers': resp.requestOptions.headers, - 'queryParams': resp.requestOptions.queryParameters, - 'body': resp.requestOptions.data, - 'response': isBytes ? 'bytes' : resp.data, - }); - } - return handler.next(resp); -}, onError: (err, handler) { - if (kDebugMode) { - Logger().e({ - 'api': ''' -${err.response?.statusCode ?? 0}: ${err.requestOptions.baseUrl}${err.requestOptions.path}''', - 'headers': err.requestOptions.headers, - 'queryParams': err.requestOptions.queryParameters, - 'body': err.requestOptions.data, - 'response': err.response?.data, - 'type': err.type - }); - } - return handler.next(err); -}); - -final options = Options( - receiveTimeout: const Duration(milliseconds: 120000), - sendTimeout: const Duration(milliseconds: 120000), -); - -const Duration globalTimeout = Duration(seconds: 15); - +@LazySingleton() class NetworkService { - final String baseUrl; + final AppEnvironment environment; final NetworkUtils networkUtils; NetworkService({ - required this.baseUrl, + required this.environment, required this.networkUtils, }); @@ -84,7 +38,7 @@ class NetworkService { headers?.addAll(headersRequest()); Response response = await dio - .get(baseUrl + path, + .get(environment.baseUrl + path, queryParameters: queryParams, options: options.copyWith( headers: headers ?? headersRequest(), @@ -104,7 +58,7 @@ class NetworkService { headers?.addAll(headersRequest()); Response response = await dio - .post(baseUrl + path, + .post(environment.baseUrl + path, queryParameters: queryParams, data: formData ?? json.encode(data), options: options.copyWith( @@ -129,7 +83,7 @@ class NetworkService { } Response response = await dio - .put(baseUrl + path, + .put(environment.baseUrl + path, queryParameters: queryParams, data: formData ?? json.encode(data), options: options.copyWith( @@ -149,7 +103,7 @@ class NetworkService { headers?.addAll(headersRequest()); Response response = await dio - .delete(baseUrl + path, + .delete(environment.baseUrl + path, queryParameters: queryParams, data: json.encode(data), options: options.copyWith( @@ -169,7 +123,7 @@ class NetworkService { headers?.addAll(headersRequest()); Response response = await dio - .patch(baseUrl + path, + .patch(environment.baseUrl + path, queryParameters: queryParams, data: json.encode(data), options: options.copyWith( @@ -179,3 +133,47 @@ class NetworkService { return response; } } + +final interceptors = QueuedInterceptorsWrapper(onRequest: (options, handler) { + if (kDebugMode) { + Logger().i({ + 'api': options.path, + 'headers': options.headers, + 'queryParams': options.queryParameters, + }); + } + return handler.next(options); +}, onResponse: (resp, handler) { + if (kDebugMode) { + final isBytes = resp.requestOptions.responseType == ResponseType.bytes; + Logger().i({ + 'api': ''' +${resp.statusCode}: ${resp.requestOptions.baseUrl}${resp.requestOptions.path}''', + 'headers': resp.requestOptions.headers, + 'queryParams': resp.requestOptions.queryParameters, + 'body': resp.requestOptions.data, + 'response': isBytes ? 'bytes' : resp.data, + }); + } + return handler.next(resp); +}, onError: (err, handler) { + if (kDebugMode) { + Logger().e({ + 'api': ''' +${err.response?.statusCode ?? 0}: ${err.requestOptions.baseUrl}${err.requestOptions.path}''', + 'headers': err.requestOptions.headers, + 'queryParams': err.requestOptions.queryParameters, + 'body': err.requestOptions.data, + 'response': err.response?.data, + 'type': err.type + }); + } + return handler.next(err); +}); + +final options = Options( + receiveTimeout: const Duration(milliseconds: 120000), + sendTimeout: const Duration(milliseconds: 120000), +); + +const Duration globalTimeout = Duration(seconds: 15); diff --git a/lib/core/client/network_utils.dart b/lib/core/client/network_utils.dart index 05c627c..ababdc2 100644 --- a/lib/core/client/network_utils.dart +++ b/lib/core/client/network_utils.dart @@ -1,6 +1,8 @@ import 'package:boilerplate/core/constants/app_key.dart'; import 'package:boilerplate/core/database/secure_database.dart'; +import 'package:injectable/injectable.dart'; +@LazySingleton() class NetworkUtils { final SecureDatabase _secureDatabase; diff --git a/lib/core/constants/app_key.dart b/lib/core/constants/app_key.dart index 56c2b86..25f4298 100644 --- a/lib/core/constants/app_key.dart +++ b/lib/core/constants/app_key.dart @@ -2,4 +2,7 @@ class AppKey { static const String token = 'token'; static const String refreshToken = 'refresh_token'; static const String isAlreadyOpen = 'is_already_open'; + + static const String devEnv = "dev"; + static const String prodEnv = "prod"; } diff --git a/lib/core/database/secure_database.dart b/lib/core/database/secure_database.dart index 2a95c47..9937c00 100644 --- a/lib/core/database/secure_database.dart +++ b/lib/core/database/secure_database.dart @@ -1,4 +1,5 @@ import 'package:boilerplate/services/secure_storage.dart'; +import 'package:injectable/injectable.dart'; abstract class SecureDatabase { Future write({ @@ -11,6 +12,7 @@ abstract class SecureDatabase { Future getString(String key); } +@LazySingleton(as: SecureDatabase) class SecureDatabaseImpl implements SecureDatabase { final SecureStorage _storage; diff --git a/lib/features/authentication/data/auth_repository_impl.dart b/lib/features/authentication/data/auth_repository_impl.dart index 77d722a..6132eef 100644 --- a/lib/features/authentication/data/auth_repository_impl.dart +++ b/lib/features/authentication/data/auth_repository_impl.dart @@ -1,14 +1,16 @@ import 'package:boilerplate/core/client/api_call.dart'; import 'package:boilerplate/core/client/network_exception.dart'; import 'package:boilerplate/features/authentication/data/local/auth_local_data_sources.dart'; -import 'package:boilerplate/features/authentication/data/model/mapper/auth_mapper.dart'; -import 'package:boilerplate/features/authentication/data/model/request/post_login_request.dart'; -import 'package:boilerplate/features/authentication/data/model/request/save_token_request.dart'; +import 'package:boilerplate/features/authentication/data/remote/model/mapper/auth_mapper.dart'; +import 'package:boilerplate/features/authentication/data/remote/model/request/post_login_request.dart'; +import 'package:boilerplate/features/authentication/data/remote/model/request/save_token_request.dart'; import 'package:boilerplate/features/authentication/data/remote/auth_remote_data_sources.dart'; import 'package:boilerplate/features/authentication/domain/model/auth.dart'; import 'package:boilerplate/features/authentication/domain/repository/auth_repository.dart'; import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; +@LazySingleton(as: AuthRepository) class AuthRepositoryImpl implements AuthRepository { final AuthRemoteDataSources _remoteDataSources; final AuthLocalDataSources _localDataSources; diff --git a/lib/features/authentication/data/local/auth_local_data_sources.dart b/lib/features/authentication/data/local/auth_local_data_sources.dart index e789a69..a2feaaf 100644 --- a/lib/features/authentication/data/local/auth_local_data_sources.dart +++ b/lib/features/authentication/data/local/auth_local_data_sources.dart @@ -1,11 +1,13 @@ import 'package:boilerplate/core/constants/app_key.dart'; import 'package:boilerplate/core/database/secure_database.dart'; -import 'package:boilerplate/features/authentication/data/model/request/save_token_request.dart'; +import 'package:boilerplate/features/authentication/data/remote/model/request/save_token_request.dart'; +import 'package:injectable/injectable.dart'; abstract class AuthLocalDataSources { Future saveToken(SaveTokenRequest request); } +@LazySingleton(as: AuthLocalDataSources) class AuthLocalDataSourcesImpl implements AuthLocalDataSources { final SecureDatabase _database; diff --git a/lib/features/authentication/data/remote/auth_remote_data_sources.dart b/lib/features/authentication/data/remote/auth_remote_data_sources.dart index e62fc18..59b3cde 100644 --- a/lib/features/authentication/data/remote/auth_remote_data_sources.dart +++ b/lib/features/authentication/data/remote/auth_remote_data_sources.dart @@ -1,13 +1,15 @@ import 'package:boilerplate/core/client/network_service.dart'; import 'package:boilerplate/core/constants/endpoints.dart'; -import 'package:boilerplate/features/authentication/data/model/request/post_login_request.dart'; +import 'package:boilerplate/features/authentication/data/remote/model/request/post_login_request.dart'; +import 'package:injectable/injectable.dart'; -import '../model/responses/auth_response.dart'; +import 'model/responses/auth_response.dart'; abstract class AuthRemoteDataSources { Future postLogin(PostLoginRequest request); } +@LazySingleton(as: AuthRemoteDataSources) class AuthRemoteDataSourceImpl implements AuthRemoteDataSources { final NetworkService networkService; diff --git a/lib/features/authentication/data/model/mapper/auth_mapper.dart b/lib/features/authentication/data/remote/model/mapper/auth_mapper.dart similarity index 92% rename from lib/features/authentication/data/model/mapper/auth_mapper.dart rename to lib/features/authentication/data/remote/model/mapper/auth_mapper.dart index e918d7b..9446276 100644 --- a/lib/features/authentication/data/model/mapper/auth_mapper.dart +++ b/lib/features/authentication/data/remote/model/mapper/auth_mapper.dart @@ -12,7 +12,7 @@ class AuthMapper { lastName: response.lastName, gender: response.gender, image: response.image, - token: response.token, + token: response.accessToken, ); } } diff --git a/lib/features/authentication/data/model/request/post_login_request.dart b/lib/features/authentication/data/remote/model/request/post_login_request.dart similarity index 100% rename from lib/features/authentication/data/model/request/post_login_request.dart rename to lib/features/authentication/data/remote/model/request/post_login_request.dart diff --git a/lib/features/authentication/data/model/request/save_token_request.dart b/lib/features/authentication/data/remote/model/request/save_token_request.dart similarity index 100% rename from lib/features/authentication/data/model/request/save_token_request.dart rename to lib/features/authentication/data/remote/model/request/save_token_request.dart diff --git a/lib/features/authentication/data/model/responses/auth_response.dart b/lib/features/authentication/data/remote/model/responses/auth_response.dart similarity index 88% rename from lib/features/authentication/data/model/responses/auth_response.dart rename to lib/features/authentication/data/remote/model/responses/auth_response.dart index fabf5a5..0293177 100644 --- a/lib/features/authentication/data/model/responses/auth_response.dart +++ b/lib/features/authentication/data/remote/model/responses/auth_response.dart @@ -13,7 +13,8 @@ class AuthResponse with _$AuthResponse { required String lastName, required String gender, required String image, - required String token, + required String accessToken, + required String refreshToken, }) = _AuthResponse; factory AuthResponse.fromJson(Map json) => diff --git a/lib/features/authentication/di/authentication_module.dart b/lib/features/authentication/di/authentication_module.dart deleted file mode 100644 index 58ab817..0000000 --- a/lib/features/authentication/di/authentication_module.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:boilerplate/core/client/network_service.dart'; -import 'package:boilerplate/core/database/secure_database.dart'; -import 'package:boilerplate/features/authentication/data/auth_repository_impl.dart'; -import 'package:boilerplate/features/authentication/data/local/auth_local_data_sources.dart'; -import 'package:boilerplate/features/authentication/data/remote/auth_remote_data_sources.dart'; -import 'package:boilerplate/features/authentication/domain/authentication_interactor.dart'; -import 'package:boilerplate/features/authentication/domain/repository/auth_repository.dart'; -import 'package:boilerplate/features/authentication/domain/use_cases/authentication_use_cases.dart'; -import 'package:boilerplate/features/authentication/presentation/blocs/authentication_bloc.dart'; -import 'package:get_it/get_it.dart'; - -void registerAuthentication(GetIt di) { - di.registerLazySingleton( - () => AuthRemoteDataSourceImpl(di())); - di.registerLazySingleton( - () => AuthLocalDataSourcesImpl( - di(), - ), - ); - di.registerLazySingleton( - () => AuthRepositoryImpl( - di(), - di(), - ), - ); - di.registerLazySingleton( - () => AuthenticationInteractor(di())); - di.registerLazySingleton( - () => AuthenticationBloc(di())); -} diff --git a/lib/features/authentication/domain/authentication_interactor.dart b/lib/features/authentication/domain/authentication_interactor.dart index bbff699..aaaa62e 100644 --- a/lib/features/authentication/domain/authentication_interactor.dart +++ b/lib/features/authentication/domain/authentication_interactor.dart @@ -3,7 +3,9 @@ import 'package:boilerplate/features/authentication/domain/model/auth.dart'; import 'package:boilerplate/features/authentication/domain/repository/auth_repository.dart'; import 'package:boilerplate/features/authentication/domain/use_cases/authentication_use_cases.dart'; import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; +@LazySingleton(as: AuthenticationUseCases) class AuthenticationInteractor implements AuthenticationUseCases { final AuthRepository _repository; diff --git a/lib/features/authentication/presentation/blocs/authentication_bloc.dart b/lib/features/authentication/presentation/blocs/authentication_bloc.dart index 16f62a0..2dfa404 100644 --- a/lib/features/authentication/presentation/blocs/authentication_bloc.dart +++ b/lib/features/authentication/presentation/blocs/authentication_bloc.dart @@ -1,5 +1,6 @@ import 'package:boilerplate/features/authentication/presentation/blocs/states/post_login_states.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; import '../../domain/use_cases/authentication_use_cases.dart'; import 'authentication_events.dart'; @@ -7,6 +8,7 @@ import 'authentication_states.dart'; import 'events/login_refresh_events.dart'; import 'events/post_login_events.dart'; +@Injectable() class AuthenticationBloc extends Bloc { final AuthenticationUseCases _useCases; @@ -22,9 +24,7 @@ class AuthenticationBloc final response = await _useCases.postLogin(event.username, event.password); await response.fold( (l) { - emitter( - PostLoginErrorState(message: l.message ?? ''), - ); + emitter(PostLoginErrorState(message: l.message ?? '')); }, (r) async { await _useCases.saveToken(r.token, r.token); diff --git a/lib/features/authentication/presentation/pages/login_page.dart b/lib/features/authentication/presentation/pages/login_page.dart index c0de6ca..96621fb 100644 --- a/lib/features/authentication/presentation/pages/login_page.dart +++ b/lib/features/authentication/presentation/pages/login_page.dart @@ -16,16 +16,26 @@ import 'package:go_router/go_router.dart'; import '../../../../services/di.dart'; -class LoginPage extends StatefulWidget { +class LoginPage extends StatelessWidget { const LoginPage({super.key}); static const route = '/login'; @override - State createState() => _LoginPageState(); + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => di(), child: const LoginPageView()); + } +} + +class LoginPageView extends StatefulWidget { + const LoginPageView({super.key}); + + @override + State createState() => _LoginPageViewState(); } -class _LoginPageState extends State { +class _LoginPageViewState extends State { late TextEditingController _nameController; late TextEditingController _passwordController; @@ -62,11 +72,8 @@ class _LoginPageState extends State { ), textAlign: TextAlign.center, ), - const SizedBox( - height: 64, - ), + const SizedBox(height: 64), BlocBuilder( - bloc: di(), buildWhen: (p, n) { return n is PostLoginErrorState || n is PostLoginInitState; }, @@ -90,24 +97,20 @@ class _LoginPageState extends State { label: 'Username', hint: 'Masukkan username', onChanged: (_) { - di().add(LoginRefreshEvent()); + context.read().add(LoginRefreshEvent()); }, ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), AppTextField( controller: _passwordController, obscureText: true, label: 'Password', hint: 'Masukkan password', onChanged: (_) { - di().add(LoginRefreshEvent()); + context.read().add(LoginRefreshEvent()); }, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Align( alignment: Alignment.centerRight, child: AppTextButton( @@ -117,11 +120,9 @@ class _LoginPageState extends State { }, ), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), BlocConsumer( - bloc: di(), + bloc: context.read(), listenWhen: (p, n) { return n is PostLoginLoadingState || n is PostLoginSuccessState || @@ -143,19 +144,17 @@ class _LoginPageState extends State { text: 'Log in', isLoading: state is PostLoginLoadingState, onTap: () { - di().add( - PostLoginEvent( - username: _nameController.text, - password: _passwordController.text, - ), - ); + context.read().add( + PostLoginEvent( + username: _nameController.text, + password: _passwordController.text, + ), + ); }, ); }, ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), ], ), ), diff --git a/lib/features/onboarding/data/onboarding_repository_impl.dart b/lib/features/onboarding/data/onboarding_repository_impl.dart index 0fb78f1..180b926 100644 --- a/lib/features/onboarding/data/onboarding_repository_impl.dart +++ b/lib/features/onboarding/data/onboarding_repository_impl.dart @@ -1,12 +1,14 @@ import 'package:boilerplate/core/client/network_exception.dart'; -import 'package:boilerplate/features/onboarding/data/model/mapper/onboarding_mapper.dart'; +import 'package:boilerplate/features/onboarding/data/remote/model/mapper/onboarding_mapper.dart'; import 'package:boilerplate/features/onboarding/data/remote/onboarding_remote_data_sources.dart'; import 'package:boilerplate/features/onboarding/domain/model/onboarding_user.dart'; import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; import '../../../core/client/api_call.dart'; import '../domain/repository/onboarding_repository.dart'; +@LazySingleton(as: OnboardingRepository) class OnboardingRepositoryImpl implements OnboardingRepository { final OnboardingRemoteDataSources _dataSources; diff --git a/lib/features/onboarding/data/model/mapper/onboarding_mapper.dart b/lib/features/onboarding/data/remote/model/mapper/onboarding_mapper.dart similarity index 82% rename from lib/features/onboarding/data/model/mapper/onboarding_mapper.dart rename to lib/features/onboarding/data/remote/model/mapper/onboarding_mapper.dart index 2c045ae..d04ccef 100644 --- a/lib/features/onboarding/data/model/mapper/onboarding_mapper.dart +++ b/lib/features/onboarding/data/remote/model/mapper/onboarding_mapper.dart @@ -1,4 +1,5 @@ -import '../../../domain/model/onboarding_user.dart'; +import 'package:boilerplate/features/onboarding/domain/model/onboarding_user.dart'; + import '../responses/onboarding_response.dart'; class OnboardingMapper { diff --git a/lib/features/onboarding/data/model/responses/onboarding_response.dart b/lib/features/onboarding/data/remote/model/responses/onboarding_response.dart similarity index 100% rename from lib/features/onboarding/data/model/responses/onboarding_response.dart rename to lib/features/onboarding/data/remote/model/responses/onboarding_response.dart diff --git a/lib/features/onboarding/data/remote/onboarding_remote_data_sources.dart b/lib/features/onboarding/data/remote/onboarding_remote_data_sources.dart index f0ad951..853956e 100644 --- a/lib/features/onboarding/data/remote/onboarding_remote_data_sources.dart +++ b/lib/features/onboarding/data/remote/onboarding_remote_data_sources.dart @@ -1,12 +1,14 @@ import 'package:boilerplate/core/client/network_service.dart'; import 'package:boilerplate/core/constants/endpoints.dart'; +import 'package:injectable/injectable.dart'; -import '../model/responses/onboarding_response.dart'; +import 'model/responses/onboarding_response.dart'; abstract class OnboardingRemoteDataSources { Future getUser(); } +@LazySingleton(as: OnboardingRemoteDataSources) class OnboardingRemoteDataSourceImpl implements OnboardingRemoteDataSources { final NetworkService networkService; diff --git a/lib/features/onboarding/di/onboarding_module.dart b/lib/features/onboarding/di/onboarding_module.dart deleted file mode 100644 index d7d3ebb..0000000 --- a/lib/features/onboarding/di/onboarding_module.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:boilerplate/core/client/network_service.dart'; -import 'package:boilerplate/features/onboarding/data/onboarding_repository_impl.dart'; -import 'package:boilerplate/features/onboarding/data/remote/onboarding_remote_data_sources.dart'; -import 'package:boilerplate/features/onboarding/domain/authentication_interactor.dart'; -import 'package:boilerplate/features/onboarding/domain/repository/onboarding_repository.dart'; -import 'package:boilerplate/features/onboarding/domain/use_cases/onboarding_use_cases.dart'; -import 'package:boilerplate/features/onboarding/presentation/blocs/onboarding_bloc.dart'; -import 'package:get_it/get_it.dart'; - -void registerOnboarding(GetIt di) { - di.registerFactory( - () => OnboardingRemoteDataSourceImpl(di())); - di.registerFactory( - () => OnboardingRepositoryImpl(di())); - di.registerFactory( - () => OnboardingInteractor(di())); - di.registerLazySingleton( - () => OnboardingBloc(di())); -} diff --git a/lib/features/onboarding/domain/authentication_interactor.dart b/lib/features/onboarding/domain/authentication_interactor.dart index 5bf6f01..8f87e14 100644 --- a/lib/features/onboarding/domain/authentication_interactor.dart +++ b/lib/features/onboarding/domain/authentication_interactor.dart @@ -3,7 +3,9 @@ import 'package:boilerplate/features/onboarding/domain/model/onboarding_user.dar import 'package:boilerplate/features/onboarding/domain/repository/onboarding_repository.dart'; import 'package:boilerplate/features/onboarding/domain/use_cases/onboarding_use_cases.dart'; import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; +@LazySingleton(as: OnboardingUseCases) class OnboardingInteractor implements OnboardingUseCases { final OnboardingRepository _repository; diff --git a/lib/features/onboarding/presentation/blocs/onboarding_bloc.dart b/lib/features/onboarding/presentation/blocs/onboarding_bloc.dart index 8f1b410..806bcc7 100644 --- a/lib/features/onboarding/presentation/blocs/onboarding_bloc.dart +++ b/lib/features/onboarding/presentation/blocs/onboarding_bloc.dart @@ -5,9 +5,11 @@ import 'package:boilerplate/features/onboarding/presentation/blocs/onboarding_ev import 'package:boilerplate/features/onboarding/presentation/blocs/onboarding_states.dart'; import 'package:boilerplate/features/onboarding/presentation/blocs/states/onboarding_states.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; import '../../domain/use_cases/onboarding_use_cases.dart'; +@Injectable() class OnboardingBloc extends Bloc { final OnboardingUseCases _useCases; diff --git a/lib/features/onboarding/presentation/pages/onboarding_page.dart b/lib/features/onboarding/presentation/pages/onboarding_page.dart index ea19d2d..ae2461f 100644 --- a/lib/features/onboarding/presentation/pages/onboarding_page.dart +++ b/lib/features/onboarding/presentation/pages/onboarding_page.dart @@ -100,7 +100,7 @@ class _OnboardingPageState extends State { ); return; } - context.pushReplacementNamed(LoginPage.route); + context.goNamed(LoginPage.route); }, ); }, diff --git a/lib/features/onboarding/presentation/pages/splash_page.dart b/lib/features/onboarding/presentation/pages/splash_page.dart index 656a9dc..c07a54b 100644 --- a/lib/features/onboarding/presentation/pages/splash_page.dart +++ b/lib/features/onboarding/presentation/pages/splash_page.dart @@ -14,32 +14,36 @@ import '../../../../design/constants/colors.dart'; import '../../../../design/constants/text_style.dart'; import '../../../../services/di.dart'; -class SplashPage extends StatefulWidget { +class SplashPage extends StatelessWidget { const SplashPage({super.key}); static const route = '/splash'; @override - State createState() => _SplashPageState(); + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => di(), child: const SplashPageView()); + } } -class _SplashPageState extends State { +class SplashPageView extends StatefulWidget { + const SplashPageView({super.key}); + + @override + State createState() => _SplashPageViewState(); +} + +class _SplashPageViewState extends State { @override void initState() { - di().add(GetUserEvent()); super.initState(); + context.read().add(GetUserEvent()); } @override Widget build(BuildContext context) { return Scaffold( body: BlocListener( - bloc: di(), - listenWhen: (prev, next) { - return next is OnboardingLoggedState || - next is OnboardingNewState || - next is OnboardingErrorState; - }, listener: (context, state) { log(state.toString()); if (state is OnboardingLoggedState) { diff --git a/lib/features/product/data/product_repository_impl.dart b/lib/features/product/data/product_repository_impl.dart index e432afb..6df4f94 100644 --- a/lib/features/product/data/product_repository_impl.dart +++ b/lib/features/product/data/product_repository_impl.dart @@ -1,13 +1,15 @@ import 'package:boilerplate/core/client/network_exception.dart'; -import 'package:boilerplate/features/product/data/model/request/get_list_product_request.dart'; +import 'package:boilerplate/features/product/data/remote/model/mapper/product_mapper.dart'; +import 'package:boilerplate/features/product/data/remote/model/request/get_list_product_request.dart'; import 'package:boilerplate/features/product/data/remote/product_remote_data_sources.dart'; import 'package:boilerplate/features/product/domain/model/product.dart'; import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; import '../../../core/client/api_call.dart'; import '../domain/repository/product_repository.dart'; -import 'model/mapper/product_mapper.dart'; +@LazySingleton(as: ProductRepository) class ProductRepositoryImpl implements ProductRepository { final ProductRemoteDataSources _dataSources; diff --git a/lib/features/product/data/model/mapper/product_mapper.dart b/lib/features/product/data/remote/model/mapper/product_mapper.dart similarity index 80% rename from lib/features/product/data/model/mapper/product_mapper.dart rename to lib/features/product/data/remote/model/mapper/product_mapper.dart index 00a99a2..3b09d11 100644 --- a/lib/features/product/data/model/mapper/product_mapper.dart +++ b/lib/features/product/data/remote/model/mapper/product_mapper.dart @@ -1,6 +1,7 @@ -import 'package:boilerplate/features/product/data/model/responses/list_product_response.dart'; -import '../../../domain/model/product.dart'; +import 'package:boilerplate/features/product/data/remote/model/responses/list_product_response.dart'; +import 'package:boilerplate/features/product/domain/model/product.dart'; + import '../responses/product_item_response.dart'; class ProductMapper { diff --git a/lib/features/product/data/model/request/get_list_product_request.dart b/lib/features/product/data/remote/model/request/get_list_product_request.dart similarity index 100% rename from lib/features/product/data/model/request/get_list_product_request.dart rename to lib/features/product/data/remote/model/request/get_list_product_request.dart diff --git a/lib/features/product/data/model/responses/list_product_response.dart b/lib/features/product/data/remote/model/responses/list_product_response.dart similarity index 83% rename from lib/features/product/data/model/responses/list_product_response.dart rename to lib/features/product/data/remote/model/responses/list_product_response.dart index 255cffb..2593a54 100644 --- a/lib/features/product/data/model/responses/list_product_response.dart +++ b/lib/features/product/data/remote/model/responses/list_product_response.dart @@ -1,4 +1,4 @@ -import 'package:boilerplate/features/product/data/model/responses/product_item_response.dart'; +import 'package:boilerplate/features/product/data/remote/model/responses/product_item_response.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'list_product_response.freezed.dart'; diff --git a/lib/features/product/data/model/responses/product_item_response.dart b/lib/features/product/data/remote/model/responses/product_item_response.dart similarity index 100% rename from lib/features/product/data/model/responses/product_item_response.dart rename to lib/features/product/data/remote/model/responses/product_item_response.dart diff --git a/lib/features/product/data/model/responses/product_user_response.dart b/lib/features/product/data/remote/model/responses/product_user_response.dart similarity index 100% rename from lib/features/product/data/model/responses/product_user_response.dart rename to lib/features/product/data/remote/model/responses/product_user_response.dart diff --git a/lib/features/product/data/remote/product_remote_data_sources.dart b/lib/features/product/data/remote/product_remote_data_sources.dart index ccfbd7e..abc780a 100644 --- a/lib/features/product/data/remote/product_remote_data_sources.dart +++ b/lib/features/product/data/remote/product_remote_data_sources.dart @@ -1,9 +1,11 @@ import 'package:boilerplate/core/client/network_service.dart'; import 'package:boilerplate/core/constants/endpoints.dart'; -import 'package:boilerplate/features/product/data/model/request/get_list_product_request.dart'; -import 'package:boilerplate/features/product/data/model/responses/list_product_response.dart'; +import 'package:boilerplate/features/product/data/remote/model/request/get_list_product_request.dart'; +import 'package:boilerplate/features/product/data/remote/model/responses/list_product_response.dart'; +import 'package:injectable/injectable.dart'; + +import 'model/responses/product_user_response.dart'; -import '../model/responses/product_user_response.dart'; abstract class ProductRemoteDataSources { Future getUser(); @@ -11,6 +13,7 @@ abstract class ProductRemoteDataSources { Future getProducts(GetListProductRequest request); } +@LazySingleton(as: ProductRemoteDataSources) class ProductRemoteDataSourceImpl implements ProductRemoteDataSources { final NetworkService networkService; diff --git a/lib/features/product/di/product_module.dart b/lib/features/product/di/product_module.dart deleted file mode 100644 index 3a27093..0000000 --- a/lib/features/product/di/product_module.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:boilerplate/core/client/network_service.dart'; -import 'package:boilerplate/features/product/data/product_repository_impl.dart'; -import 'package:boilerplate/features/product/data/remote/product_remote_data_sources.dart'; -import 'package:boilerplate/features/product/domain/product_interactor.dart'; -import 'package:boilerplate/features/product/domain/repository/product_repository.dart'; -import 'package:boilerplate/features/product/domain/use_cases/product_use_cases.dart'; -import 'package:boilerplate/features/product/presentation/home/blocs/product_home_bloc.dart'; -import 'package:get_it/get_it.dart'; - -void registerProduct(GetIt di) { - di.registerFactory( - () => ProductRemoteDataSourceImpl(di())); - di.registerFactory( - () => ProductRepositoryImpl(di())); - di.registerFactory( - () => ProductInteractor(di())); - di.registerLazySingleton( - () => ProductHomeBloc(di())); -} diff --git a/lib/features/product/domain/product_interactor.dart b/lib/features/product/domain/product_interactor.dart index 495d261..7e7975f 100644 --- a/lib/features/product/domain/product_interactor.dart +++ b/lib/features/product/domain/product_interactor.dart @@ -3,7 +3,9 @@ import 'package:boilerplate/features/product/domain/model/product.dart'; import 'package:boilerplate/features/product/domain/repository/product_repository.dart'; import 'package:boilerplate/features/product/domain/use_cases/product_use_cases.dart'; import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; +@LazySingleton(as: ProductUseCases) class ProductInteractor implements ProductUseCases { final ProductRepository _repository; ProductInteractor(this._repository); diff --git a/lib/features/product/presentation/home/blocs/product_home_bloc.dart b/lib/features/product/presentation/home/blocs/product_home_bloc.dart index 5cdc5c1..e6f78b7 100644 --- a/lib/features/product/presentation/home/blocs/product_home_bloc.dart +++ b/lib/features/product/presentation/home/blocs/product_home_bloc.dart @@ -3,9 +3,11 @@ import 'package:boilerplate/features/product/presentation/home/blocs/product_hom import 'package:boilerplate/features/product/presentation/home/blocs/product_home_states.dart'; import 'package:boilerplate/features/product/presentation/home/blocs/states/get_home_product_states.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; import '../../../domain/use_cases/product_use_cases.dart'; +@Injectable() class ProductHomeBloc extends Bloc { final ProductUseCases _useCases; @@ -20,12 +22,10 @@ class ProductHomeBloc extends Bloc { emitter(GetHomeProductLoadingState()); final response = await _useCases.getProducts(6, 0); response.fold( - (l) { - emitter( - GetHomeProductErrorState(message: l.message ?? ''), - ); + (l) { + emitter(GetHomeProductErrorState(message: l.message ?? '')); }, - (r) { + (r) { emitter(GetHomeProductSuccessState(products: r)); }, ); diff --git a/lib/features/profile/data/local/profile_local_data_sources.dart b/lib/features/profile/data/local/profile_local_data_sources.dart index 8067924..1db887e 100644 --- a/lib/features/profile/data/local/profile_local_data_sources.dart +++ b/lib/features/profile/data/local/profile_local_data_sources.dart @@ -1,10 +1,12 @@ import 'package:boilerplate/core/constants/app_key.dart'; import 'package:boilerplate/core/database/secure_database.dart'; +import 'package:injectable/injectable.dart'; abstract class ProfileLocalDataSources { Future logOut(); } +@LazySingleton(as: ProfileLocalDataSources) class ProfileLocalDataSourcesImpl implements ProfileLocalDataSources { final SecureDatabase _secureDatabase; diff --git a/lib/features/profile/data/profile_repository_impl.dart b/lib/features/profile/data/profile_repository_impl.dart index b5276a1..61e18aa 100644 --- a/lib/features/profile/data/profile_repository_impl.dart +++ b/lib/features/profile/data/profile_repository_impl.dart @@ -1,13 +1,15 @@ import 'package:boilerplate/core/client/network_exception.dart'; import 'package:boilerplate/features/profile/data/local/profile_local_data_sources.dart'; +import 'package:boilerplate/features/profile/data/remote/model/mapper/profile_mapper.dart'; import 'package:boilerplate/features/profile/data/remote/profile_remote_data_sources.dart'; import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; import '../../../core/client/api_call.dart'; import '../domain/model/user.dart'; import '../domain/repository/profile_repository.dart'; -import 'model/mapper/profile_mapper.dart'; +@LazySingleton(as: ProfileRepository) class ProfileRepositoryImpl implements ProfileRepository { final ProfileRemoteDataSources _remoteDataSources; final ProfileLocalDataSources _localDataSources; diff --git a/lib/features/profile/data/model/mapper/profile_mapper.dart b/lib/features/profile/data/remote/model/mapper/profile_mapper.dart similarity index 84% rename from lib/features/profile/data/model/mapper/profile_mapper.dart rename to lib/features/profile/data/remote/model/mapper/profile_mapper.dart index 57dfaa7..22419d6 100644 --- a/lib/features/profile/data/model/mapper/profile_mapper.dart +++ b/lib/features/profile/data/remote/model/mapper/profile_mapper.dart @@ -1,4 +1,5 @@ -import '../../../domain/model/user.dart'; + +import 'package:boilerplate/features/profile/domain/model/user.dart'; import '../responses/user_response.dart'; class ProfileMapper { diff --git a/lib/features/profile/data/model/responses/user_response.dart b/lib/features/profile/data/remote/model/responses/user_response.dart similarity index 100% rename from lib/features/profile/data/model/responses/user_response.dart rename to lib/features/profile/data/remote/model/responses/user_response.dart diff --git a/lib/features/profile/data/remote/profile_remote_data_sources.dart b/lib/features/profile/data/remote/profile_remote_data_sources.dart index e9e8ec9..7197d74 100644 --- a/lib/features/profile/data/remote/profile_remote_data_sources.dart +++ b/lib/features/profile/data/remote/profile_remote_data_sources.dart @@ -1,12 +1,14 @@ import 'package:boilerplate/core/client/network_service.dart'; import 'package:boilerplate/core/constants/endpoints.dart'; +import 'package:injectable/injectable.dart'; -import '../model/responses/user_response.dart'; +import 'model/responses/user_response.dart'; abstract class ProfileRemoteDataSources { Future getUser(); } +@LazySingleton(as: ProfileRemoteDataSources) class ProfileRemoteDataSourceImpl implements ProfileRemoteDataSources { final NetworkService networkService; diff --git a/lib/features/profile/di/profile_module.dart b/lib/features/profile/di/profile_module.dart deleted file mode 100644 index 7dfe061..0000000 --- a/lib/features/profile/di/profile_module.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:boilerplate/core/client/network_service.dart'; -import 'package:boilerplate/core/database/secure_database.dart'; -import 'package:boilerplate/features/profile/data/local/profile_local_data_sources.dart'; -import 'package:boilerplate/features/profile/data/profile_repository_impl.dart'; -import 'package:boilerplate/features/profile/data/remote/profile_remote_data_sources.dart'; -import 'package:boilerplate/features/profile/domain/profile_interactor.dart'; -import 'package:boilerplate/features/profile/domain/repository/profile_repository.dart'; -import 'package:boilerplate/features/profile/domain/use_cases/profile_use_cases.dart'; -import 'package:boilerplate/features/profile/presentation/blocs/authentication_bloc.dart'; -import 'package:get_it/get_it.dart'; - -void registerProfile(GetIt di) { - di.registerFactory( - () => ProfileRemoteDataSourceImpl(di())); - di.registerFactory( - () => ProfileLocalDataSourcesImpl(di())); - di.registerFactory(() => ProfileRepositoryImpl( - di(), di())); - di.registerFactory( - () => ProfileInteractor(di())); - di.registerLazySingleton( - () => ProfileBloc(di())); -} diff --git a/lib/main.dart b/lib/main.dart index 97cef9b..0fa92ee 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,11 @@ import 'package:boilerplate/app.dart'; -import 'package:boilerplate/core/client/network_service.dart'; import 'package:boilerplate/services/di.dart'; import 'package:flutter/material.dart'; +import 'core/constants/app_key.dart'; + Future main() async { WidgetsFlutterBinding.ensureInitialized(); - await initLocator(NetworkServiceType.staging); + configureDependencies(environment: AppKey.devEnv); runApp(const App()); } diff --git a/lib/main_production.dart b/lib/main_production.dart index 6a791e1..07dc3f5 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -1,10 +1,11 @@ import 'package:boilerplate/app.dart'; -import 'package:boilerplate/core/client/network_service.dart'; import 'package:boilerplate/services/di.dart'; import 'package:flutter/material.dart'; +import 'core/constants/app_key.dart'; + Future main() async { WidgetsFlutterBinding.ensureInitialized(); - await initLocator(NetworkServiceType.production); + configureDependencies(environment: AppKey.prodEnv); runApp(const App()); } diff --git a/lib/services/di.config.dart b/lib/services/di.config.dart new file mode 100644 index 0000000..c997237 --- /dev/null +++ b/lib/services/di.config.dart @@ -0,0 +1,128 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// InjectableConfigGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +// coverage:ignore-file + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:get_it/get_it.dart' as _i174; +import 'package:injectable/injectable.dart' as _i526; + +import '../core/client/app_environment.dart' as _i119; +import '../core/client/network_service.dart' as _i941; +import '../core/client/network_utils.dart' as _i936; +import '../core/database/secure_database.dart' as _i124; +import '../features/authentication/data/auth_repository_impl.dart' as _i493; +import '../features/authentication/data/local/auth_local_data_sources.dart' + as _i981; +import '../features/authentication/data/remote/auth_remote_data_sources.dart' + as _i24; +import '../features/authentication/domain/authentication_interactor.dart' + as _i56; +import '../features/authentication/domain/repository/auth_repository.dart' + as _i888; +import '../features/authentication/domain/use_cases/authentication_use_cases.dart' + as _i521; +import '../features/authentication/presentation/blocs/authentication_bloc.dart' + as _i960; +import '../features/onboarding/data/onboarding_repository_impl.dart' as _i255; +import '../features/onboarding/data/remote/onboarding_remote_data_sources.dart' + as _i438; +import '../features/onboarding/domain/authentication_interactor.dart' as _i698; +import '../features/onboarding/domain/repository/onboarding_repository.dart' + as _i998; +import '../features/onboarding/domain/use_cases/onboarding_use_cases.dart' + as _i1022; +import '../features/onboarding/presentation/blocs/onboarding_bloc.dart' + as _i221; +import '../features/product/data/product_repository_impl.dart' as _i162; +import '../features/product/data/remote/product_remote_data_sources.dart' + as _i174; +import '../features/product/domain/product_interactor.dart' as _i283; +import '../features/product/domain/repository/product_repository.dart' as _i128; +import '../features/product/domain/use_cases/product_use_cases.dart' as _i60; +import '../features/product/presentation/home/blocs/product_home_bloc.dart' + as _i513; +import '../features/profile/data/local/profile_local_data_sources.dart' + as _i1024; +import '../features/profile/data/profile_repository_impl.dart' as _i1030; +import '../features/profile/data/remote/profile_remote_data_sources.dart' + as _i622; +import '../features/profile/domain/repository/profile_repository.dart' as _i928; +import 'secure_storage.dart' as _i897; + +const String _dev = 'dev'; +const String _prod = 'prod'; + +extension GetItInjectableX on _i174.GetIt { +// initializes the registration of main-scope dependencies inside of GetIt + _i174.GetIt init({ + String? environment, + _i526.EnvironmentFilter? environmentFilter, + }) { + final gh = _i526.GetItHelper( + this, + environment, + environmentFilter, + ); + gh.factory<_i897.SecureStorage>(() => _i897.SecureStorage()); + gh.factory<_i119.AppEnvironment>( + () => _i119.DevEnvironment(), + registerFor: {_dev}, + ); + gh.lazySingleton<_i124.SecureDatabase>( + () => _i124.SecureDatabaseImpl(gh<_i897.SecureStorage>())); + gh.lazySingleton<_i1024.ProfileLocalDataSources>( + () => _i1024.ProfileLocalDataSourcesImpl(gh<_i124.SecureDatabase>())); + gh.lazySingleton<_i981.AuthLocalDataSources>( + () => _i981.AuthLocalDataSourcesImpl(gh<_i124.SecureDatabase>())); + gh.factory<_i119.AppEnvironment>( + () => _i119.ProdEnvironment(), + registerFor: {_prod}, + ); + gh.lazySingleton<_i936.NetworkUtils>( + () => _i936.NetworkUtils(gh<_i124.SecureDatabase>())); + gh.lazySingleton<_i941.NetworkService>(() => _i941.NetworkService( + environment: gh<_i119.AppEnvironment>(), + networkUtils: gh<_i936.NetworkUtils>(), + )); + gh.lazySingleton<_i622.ProfileRemoteDataSources>( + () => _i622.ProfileRemoteDataSourceImpl(gh<_i941.NetworkService>())); + gh.lazySingleton<_i24.AuthRemoteDataSources>( + () => _i24.AuthRemoteDataSourceImpl(gh<_i941.NetworkService>())); + gh.lazySingleton<_i174.ProductRemoteDataSources>( + () => _i174.ProductRemoteDataSourceImpl(gh<_i941.NetworkService>())); + gh.lazySingleton<_i888.AuthRepository>(() => _i493.AuthRepositoryImpl( + gh<_i24.AuthRemoteDataSources>(), + gh<_i981.AuthLocalDataSources>(), + )); + gh.lazySingleton<_i128.ProductRepository>(() => + _i162.ProductRepositoryImpl(gh<_i174.ProductRemoteDataSources>())); + gh.lazySingleton<_i60.ProductUseCases>( + () => _i283.ProductInteractor(gh<_i128.ProductRepository>())); + gh.lazySingleton<_i438.OnboardingRemoteDataSources>( + () => _i438.OnboardingRemoteDataSourceImpl(gh<_i941.NetworkService>())); + gh.lazySingleton<_i928.ProfileRepository>( + () => _i1030.ProfileRepositoryImpl( + gh<_i622.ProfileRemoteDataSources>(), + gh<_i1024.ProfileLocalDataSources>(), + )); + gh.lazySingleton<_i998.OnboardingRepository>(() => + _i255.OnboardingRepositoryImpl( + gh<_i438.OnboardingRemoteDataSources>())); + gh.factory<_i513.ProductHomeBloc>( + () => _i513.ProductHomeBloc(gh<_i60.ProductUseCases>())); + gh.lazySingleton<_i1022.OnboardingUseCases>( + () => _i698.OnboardingInteractor(gh<_i998.OnboardingRepository>())); + gh.lazySingleton<_i521.AuthenticationUseCases>( + () => _i56.AuthenticationInteractor(gh<_i888.AuthRepository>())); + gh.factory<_i960.AuthenticationBloc>( + () => _i960.AuthenticationBloc(gh<_i521.AuthenticationUseCases>())); + gh.factory<_i221.OnboardingBloc>( + () => _i221.OnboardingBloc(gh<_i1022.OnboardingUseCases>())); + return this; + } +} diff --git a/lib/services/di.dart b/lib/services/di.dart index 37a73b0..0e25f0c 100644 --- a/lib/services/di.dart +++ b/lib/services/di.dart @@ -1,66 +1,18 @@ import 'package:boilerplate/core/client/network_service.dart'; +import 'package:boilerplate/core/client/network_utils.dart'; import 'package:boilerplate/core/constants/secrets.dart'; -import 'package:boilerplate/core/database/secure_database.dart'; -import 'package:boilerplate/features/authentication/di/authentication_module.dart'; -import 'package:boilerplate/features/onboarding/di/onboarding_module.dart'; -import 'package:boilerplate/features/product/di/product_module.dart'; -import 'package:boilerplate/features/profile/di/profile_module.dart'; -import 'package:boilerplate/services/secure_storage.dart'; import 'package:get_it/get_it.dart'; +import 'package:injectable/injectable.dart'; -import '../core/client/network_utils.dart'; +import 'di.config.dart'; -final di = GetIt.I; +final di = GetIt.instance; -Future initLocator(NetworkServiceType type) async { - await initCore(type); - initFeatures(); -} - -Future initCore(NetworkServiceType type) async { - initStorage(); - await initNetwork(type); -} - -void initFeatures() { - registerOnboarding(di); - registerAuthentication(di); - registerProfile(di); - registerProduct(di); -} - -Future initNetwork(NetworkServiceType type) async { - di.registerLazySingleton( - () => NetworkUtils( - di(), - ), - ); - await di().init(); - - switch (type) { - case NetworkServiceType.production: - di.registerLazySingleton( - () => NetworkService( - baseUrl: Secret.baseUrlProd, - networkUtils: di(), - ), - ); - break; - case NetworkServiceType.staging: - di.registerLazySingleton( - () => NetworkService( - baseUrl: Secret.baseUrlDev, - networkUtils: di(), - ), - ); - } -} - -void initStorage() { - di.registerLazySingleton(() => SecureStorage()); - di.registerLazySingleton( - () => SecureDatabaseImpl( - di(), - ), - ); +@InjectableInit( + initializerName: 'init', // default + preferRelativeImports: true, // default + asExtension: true, // default +) +void configureDependencies({required String environment}) { + di.init(environment: environment); } diff --git a/lib/services/secure_storage.dart b/lib/services/secure_storage.dart index 75e477e..e7d5524 100644 --- a/lib/services/secure_storage.dart +++ b/lib/services/secure_storage.dart @@ -1,5 +1,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:injectable/injectable.dart'; +@Injectable() class SecureStorage { final storage = const FlutterSecureStorage(); diff --git a/pubspec.lock b/pubspec.lock index 92e6304..994a67c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -485,6 +485,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + injectable: + dependency: "direct main" + description: + name: injectable + sha256: "5e1556ea1d374fe44cbe846414d9bab346285d3d8a1da5877c01ad0774006068" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + injectable_generator: + dependency: "direct dev" + description: + name: injectable_generator + sha256: af403d76c7b18b4217335e0075e950cd0579fd7f8d7bd47ee7c85ada31680ba1 + url: "https://pub.dev" + source: hosted + version: "2.6.2" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 76d2aa5..5153ca5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: google_fonts: ^6.2.1 # I infinite_scroll_pagination: ^4.1.0 + injectable: ^2.5.0 # J json_annotation: ^4.8.1 # L @@ -67,10 +68,11 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.6 - json_serializable: ^6.7.1 - freezed: ^2.4.1 + build_runner: ^2.4.14 envied_generator: ^1.0.0 + freezed: ^2.4.1 + injectable_generator: ^2.6.2 + json_serializable: ^6.9.0 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is From 2bf0e66decfaa6c32998cf0e5b2d2b1f9d77dfe9 Mon Sep 17 00:00:00 2001 From: MHibriziF Date: Sun, 15 Mar 2026 22:24:41 +0700 Subject: [PATCH 04/10] feat: implement product line variations, upgrade version --- .gitignore | 3 + README.md | 85 +- SPL.md | 254 ++++ analysis_options.yaml | 6 + ...name.snakeCase()}}_local_data_sources.dart | 24 + .../mapper/{{name.snakeCase()}}_mapper.dart | 8 + .../{{name.snakeCase()}}_response.dart | 14 + ...ame.snakeCase()}}_remote_data_sources.dart | 20 + .../{{name.snakeCase()}}_repository_impl.dart | 26 + .../domain/model/{{name.snakeCase()}}.dart | 4 + .../{{name.snakeCase()}}_repository.dart | 8 + .../{{name.snakeCase()}}_use_cases.dart | 8 + .../{{name.snakeCase()}}_interactor.dart | 16 + .../blocs/{{name.snakeCase()}}_bloc.dart | 27 + .../blocs/{{name.snakeCase()}}_event.dart | 12 + .../blocs/{{name.snakeCase()}}_state.dart | 32 + .../pages/{{name.snakeCase()}}_page.dart | 14 + bricks/feature/brick.yaml | 15 + .../storage/impl/hive_storage_provider.dart | 32 + bricks/storage_hive/brick.yaml | 6 + .../impl/shared_prefs_storage_provider.dart | 40 + bricks/storage_prefs/brick.yaml | 6 + .../storage/impl/secure_storage_provider.dart | 36 + bricks/storage_secure/brick.yaml | 6 + .../impl/sqflite_storage_provider.dart | 56 + bricks/storage_sqflite/brick.yaml | 6 + codegen/module_generator.dart | 422 ------ codegen/spl_manager.dart | 1278 +++++++++++++++++ lib/core/client/network_exception.dart | 24 +- lib/core/database/secure_database.dart | 28 +- lib/core/storage/app_storage.dart | 25 + .../storage/impl/secure_storage_provider.dart | 30 + lib/core/storage/storage_module.dart | 18 + lib/design/widgets/atom/app_text_field.dart | 4 +- lib/design/widgets/atom/primary_button.dart | 4 +- .../remote/model/responses/auth_response.dart | 2 +- .../main/presentation/pages/main_page.dart | 2 +- .../model/responses/onboarding_response.dart | 2 +- .../responses/list_product_response.dart | 2 +- .../responses/product_item_response.dart | 2 +- .../responses/product_user_response.dart | 2 +- .../home/widgets/product_item_widget.dart | 1 - .../remote/model/responses/user_response.dart | 2 +- lib/services/di.config.dart | 47 +- lib/services/di.dart | 3 - lib/services/secure_storage.dart | 22 - mason.yaml | 14 + pubspec.lock | 493 ++++--- pubspec.yaml | 22 +- spl.yaml | 71 + 50 files changed, 2499 insertions(+), 785 deletions(-) create mode 100644 SPL.md create mode 100644 bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/local/{{name.snakeCase()}}_local_data_sources.dart create mode 100644 bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/model/mapper/{{name.snakeCase()}}_mapper.dart create mode 100644 bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/model/responses/{{name.snakeCase()}}_response.dart create mode 100644 bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/remote/{{name.snakeCase()}}_remote_data_sources.dart create mode 100644 bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/{{name.snakeCase()}}_repository_impl.dart create mode 100644 bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/model/{{name.snakeCase()}}.dart create mode 100644 bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/repository/{{name.snakeCase()}}_repository.dart create mode 100644 bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/use_cases/{{name.snakeCase()}}_use_cases.dart create mode 100644 bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/{{name.snakeCase()}}_interactor.dart create mode 100644 bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/blocs/{{name.snakeCase()}}_bloc.dart create mode 100644 bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/blocs/{{name.snakeCase()}}_event.dart create mode 100644 bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/blocs/{{name.snakeCase()}}_state.dart create mode 100644 bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/pages/{{name.snakeCase()}}_page.dart create mode 100644 bricks/feature/brick.yaml create mode 100644 bricks/storage_hive/__brick__/lib/core/storage/impl/hive_storage_provider.dart create mode 100644 bricks/storage_hive/brick.yaml create mode 100644 bricks/storage_prefs/__brick__/lib/core/storage/impl/shared_prefs_storage_provider.dart create mode 100644 bricks/storage_prefs/brick.yaml create mode 100644 bricks/storage_secure/__brick__/lib/core/storage/impl/secure_storage_provider.dart create mode 100644 bricks/storage_secure/brick.yaml create mode 100644 bricks/storage_sqflite/__brick__/lib/core/storage/impl/sqflite_storage_provider.dart create mode 100644 bricks/storage_sqflite/brick.yaml delete mode 100644 codegen/module_generator.dart create mode 100644 codegen/spl_manager.dart create mode 100644 lib/core/storage/app_storage.dart create mode 100644 lib/core/storage/impl/secure_storage_provider.dart create mode 100644 lib/core/storage/storage_module.dart delete mode 100644 lib/services/secure_storage.dart create mode 100644 mason.yaml create mode 100644 spl.yaml diff --git a/.gitignore b/.gitignore index 5961c79..29780d2 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ app.*.map.json *.freezed.dart *.g.dart .env + +# AI assistants +.claude diff --git a/README.md b/README.md index 9c24d4b..72c8bd9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # 👨‍💻 Flutter Boilerplate + [![Generic badge](https://img.shields.io/badge/Flutter-v3.27.1-blue)](https://flutter.dev/docs) [![Generic badge](https://img.shields.io/badge/Dart-v3.6.0-blue)](https://dart.dev/guides) @@ -11,6 +12,7 @@ Flutter Template Add .env file to the project directory (see .env.example) Example how to run development app + ``` flutter clean flutter pub get @@ -19,6 +21,7 @@ flutter run ``` Example how to run production app + ``` flutter clean flutter pub get @@ -37,29 +40,57 @@ Reso coder's flutter clean architecture ![alt text](https://i0.wp.com/resocoder.com/wp-content/uploads/2019/08/Clean-Architecture-Flutter-Diagram.png?ssl=1) -### ⚡️ Module generator +### 🧬️ State Management + +Since we use clean architecture, it doesn't matter what state management you prefer to use + +But for this project example, we use [Bloc](https://pub.dev/packages/flutter_bloc) -In order to minimize your effort in repetitive code writing, you can use the module_generator.dart +### 🧩 Software Product Line (SPL) -simply run +This template includes an SPL layer for managing variability — swappable storage backends and state management solutions, plus a CLI to add/remove/toggle features. + +See [SPL.md](SPL.md) for the full guide. ``` -dart run .\codegen\module_generator.dart +dart run codegen/spl_manager.dart list +dart run codegen/spl_manager.dart add +dart run codegen/spl_manager.dart disable +dart run codegen/spl_manager.dart enable +dart run codegen/spl_manager.dart storage set hive +dart run codegen/spl_manager.dart state set cubit ``` -and insert your module name (example: catalog) +### 🧱 Mason (Code Generation) -### 🧬️ State Management +This project uses [Mason](https://pub.dev/packages/mason_cli) for brick-based code generation. Bricks are stored in `bricks/` as real `.dart` files with Mustache variables. + +Install Mason globally (one-time): + +``` +dart pub global activate mason_cli +``` + +Initialize bricks for this project (one-time, after `flutter pub get`): -Since we use clean architecture, it doesn't matter what state management you prefer to use +``` +mason get +``` -But for this project example, we use [Bloc](https://pub.dev/packages/flutter_bloc) +The SPL CLI (`spl_manager.dart`) uses Mason automatically when available. You can also invoke bricks directly: + +``` +mason make feature --name \ +mason make feature --name \ --with_storage true --state cubit +``` + +Bricks are excluded from Dart analysis (`analysis_options.yaml`) because they contain Mustache syntax (`{{name.pascalCase()}}`), not valid Dart. ### API Documentation [DummyJson](https://dummyjson.com/docs/) -This boilerplate use DummyJson for remote data sources +This boilerplate use DummyJson for remote data sources See [Auth](https://dummyjson.com/docs/auth) to obtain username and password to login in this app @@ -70,44 +101,42 @@ snake_case for file and folder. ### :capital_abcd: Git flow Commit rules: -(feat|fix|docs|style|refactor|perf|test|build|ci):\/* - -feat: A new feature +(feat|fix|docs|style|refactor|perf|test|build|ci):\/\* -fix: A bug fix +feat: A new feature -docs: Documentation only changes +fix: A bug fix -style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) +docs: Documentation only changes -refactor: A code change that neither fixes a bug nor adds a feature +style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) -perf: A code change that improves performance +refactor: A code change that neither fixes a bug nor adds a feature -test: Adding missing tests +perf: A code change that improves performance -build: Changes to the build/compilation/packaging process or auxiliary tools such as documentation generation +test: Adding missing tests -ci: Changes in the continuous integration/delivery setup +build: Changes to the build/compilation/packaging process or auxiliary tools such as documentation generation - +ci: Changes in the continuous integration/delivery setup -#### examples: +#### examples: -feat(auth): Form Login +feat(auth): Form Login -feat(product): implement product screen - -ci: refactor analysis job +feat(product): implement product screen +ci: refactor analysis job #### before push + 1. flutter analyze 2. flutter test branch rules: -(feature|hotfix|coldfix|service|integration|ui)\/\/* +(feature|hotfix|coldfix|service|integration|ui)\/\/\* ### How to contribute -To help work on this project, please refer to [CONTRIBUTING.md](CONTRIBUTING.md) \ No newline at end of file +To help work on this project, please refer to [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/SPL.md b/SPL.md new file mode 100644 index 0000000..cb3fac4 --- /dev/null +++ b/SPL.md @@ -0,0 +1,254 @@ +# Software Product Line (SPL) Guide + +This project uses Software Product Line Engineering (SPLE) to manage variability points — features and behaviors that can differ between products derived from this template. + +All variability is managed through a single CLI tool and a single config file. + +--- + +## Quick Reference + +``` +dart run codegen/spl_manager.dart list +dart run codegen/spl_manager.dart add [--with-storage] [--state bloc|cubit|riverpod] +dart run codegen/spl_manager.dart disable # deactivate, keep code +dart run codegen/spl_manager.dart enable # restore from catalog +dart run codegen/spl_manager.dart remove [--yes|-y] # hard delete +dart run codegen/spl_manager.dart storage set +dart run codegen/spl_manager.dart storage list +dart run codegen/spl_manager.dart state set +dart run codegen/spl_manager.dart state list +dart run codegen/spl_manager.dart fix +``` + +--- + +## Source of Truth: `spl.yaml` + +`spl.yaml` is the single source of truth for the product configuration. It tracks the active storage backend, the default state management solution, and all features. + +Do not edit `spl.yaml` by hand — use the CLI. The CLI updates this file, generates/deletes code, and re-wires DI automatically. + +--- + +## Variability Points + +This project has two variability points. They have different exclusivity rules. + +### 1. Storage — XOR (exactly one active) + +Controls the backend for `AppStorage`, the general-purpose local caching interface used by features that need to persist data locally (e.g., cached lists, user preferences). + +| Provider | Notes | +|---|---| +| `flutter_secure_storage` | Default. Encrypted key-value. No `init()` needed. | +| `hive` | Fast binary key-value. Requires `hive_flutter` in `pubspec.yaml` and `AppStorage.init()` before `runApp()`. | +| `sqflite` | SQLite. Requires `AppStorage.init()` before `runApp()`. | +| `shared_preferences` | Simple unencrypted key-value. Requires `shared_preferences` in `pubspec.yaml` and `AppStorage.init()` before `runApp()`. | + +**XOR means**: switching providers deletes the old implementation file and generates the new one. Only the active provider's impl file exists in `lib/core/storage/impl/`. + +``` +dart run codegen/spl_manager.dart storage set hive +``` + +This regenerates `lib/core/storage/impl/hive_storage_provider.dart`, rewrites `lib/core/storage/storage_module.dart` to wire the new impl, and runs `build_runner`. + +### 2. State Management — OR (global default + per-feature override) + +Controls the presentation layer pattern for features. Unlike storage, this is **not exclusive** — different features in the same app can use different state management solutions. + +| Solution | Package | Files generated | Use when | +|---|---|---|---| +| `bloc` | `flutter_bloc` | `_event.dart`, `_state.dart`, `_bloc.dart` | Complex flows with explicit event streams | +| `cubit` | `flutter_bloc` (same package) | `_state.dart`, `_cubit.dart` | Simpler flows, fewer files, methods called directly | +| `riverpod` | `flutter_riverpod` | `_state.dart`, `_notifier.dart` | Riverpod-native UIs; bridges to get_it DI via `di()` | + +Change the global default (affects all future `add` commands): +``` +dart run codegen/spl_manager.dart state set cubit +``` + +Override per feature at creation time: +``` +dart run codegen/spl_manager.dart add orders --state riverpod +``` + +Bloc and cubit coexist with zero config (same `flutter_bloc` package). Riverpod requires: +1. `flutter_riverpod` in `pubspec.yaml` +2. `ProviderScope` wrapping your root widget in `main.dart` + +--- + +## Storage Architecture: Two Separate Concerns + +There are two storage abstractions in this project. They solve different problems and are **not interchangeable**. + +### `SecureDatabase` — encrypted token store (NOT a variability point) + +``` +lib/core/database/secure_database.dart +``` + +Always backed by `FlutterSecureStorage`. **This is intentional and not configurable.** + +`FlutterSecureStorage` writes to the OS keychain (iOS Keychain / Android Keystore), providing hardware-backed encryption. Auth tokens and refresh tokens stored here cannot be read even if someone extracts the device's data directory. + +The alternative backends (Hive, SQLite, SharedPreferences) write plaintext or weakly-encrypted files to disk. Storing auth tokens in any of them would be a security vulnerability. + +**What uses it:** +- `AuthLocalDataSources` — saves access token + refresh token after login +- `ProfileLocalDataSources` — deletes both tokens on logout + +**Rule:** Only use `SecureDatabase` for secrets (tokens, keys, credentials). For everything else, use `AppStorage`. + +### `AppStorage` — general feature local cache (variability point) + +``` +lib/core/storage/app_storage.dart +lib/core/storage/impl/.dart ← only one file exists at a time +lib/core/storage/storage_module.dart ← SPL-managed, do not edit manually +``` + +Used by features that need to cache data locally — product lists, user preferences, onboarding state, etc. The backend is switchable via `storage set`. No security guarantee is assumed. + +Inject it in your local data source: +```dart +@LazySingleton(as: MyLocalDataSources) +class MyLocalDataSourcesImpl implements MyLocalDataSources { + final AppStorage _storage; + MyLocalDataSourcesImpl(this._storage); +} +``` + +Add a feature with local storage pre-wired: +``` +dart run codegen/spl_manager.dart add orders --with-storage +``` + +--- + +## Feature Management + +### Feature lifecycle + +``` +add → active ──disable──→ catalog ──enable──→ active + └──remove──→ gone forever +``` + +Features have three states: +- **Active** — code lives in `lib/features//`, compiled, DI-wired +- **Catalog** — code lives in `features_catalog//`, not compiled, not in DI, fully preserved +- **Removed** — hard deleted, gone + +`features_catalog/` is excluded from Dart analysis so inactive features never cause compile errors. + +### Adding a feature + +``` +dart run codegen/spl_manager.dart add +dart run codegen/spl_manager.dart add --with-storage +dart run codegen/spl_manager.dart add --state cubit +dart run codegen/spl_manager.dart add --with-storage --state riverpod +``` + +This scaffolds a full clean architecture feature: + +``` +lib/features// + data/ + local/_local_data_sources.dart (only with --with-storage) + model/ + mapper/_mapper.dart + responses/_response.dart + remote/_remote_data_sources.dart + _repository_impl.dart + domain/ + model/.dart + repository/_repository.dart + use_cases/_use_cases.dart + _interactor.dart + presentation/ + pages/_page.dart + blocs/ (bloc or cubit) + _event.dart (bloc only) + _state.dart + _bloc.dart | _cubit.dart + providers/ (riverpod only) + _state.dart + _notifier.dart +``` + +After scaffolding, register the route manually in `lib/core/router/app_router_config.dart`. + +DI is auto-wired — `build_runner` regenerates `lib/services/di.config.dart` automatically. + +### Disabling a feature + +``` +dart run codegen/spl_manager.dart disable +``` + +Moves `lib/features//` to `features_catalog//`, marks it `inactive` in `spl.yaml`, and regenerates DI. The code is fully preserved — nothing is deleted. + +### Re-enabling a feature + +``` +dart run codegen/spl_manager.dart enable +``` + +Moves `features_catalog//` back to `lib/features//`, marks it `active` in `spl.yaml`, and re-wires DI. All original code is restored exactly as it was left. + +### Hard deleting a feature + +``` +dart run codegen/spl_manager.dart remove +dart run codegen/spl_manager.dart remove --yes +``` + +Permanently deletes the feature from wherever it lives (active or catalog) and removes it from `spl.yaml`. Irreversible. Add `--yes` (or `-y`) to skip the confirmation prompt. + +--- + +## Template Features + +The four features included in this template (`authentication`, `onboarding`, `product`, `profile`) are working demonstrations of the architecture. They use the DummyJson API and show real usage of the data/domain/presentation layers. + +Keep them as reference — remove them when you no longer need the examples: + +``` +dart run codegen/spl_manager.dart remove authentication --yes +dart run codegen/spl_manager.dart remove onboarding --yes +dart run codegen/spl_manager.dart remove product --yes +dart run codegen/spl_manager.dart remove profile --yes +``` + +--- + +## Mason Bricks + +The CLI uses [Mason](https://pub.dev/packages/mason_cli) for code generation if available, otherwise falls back to inline templates. + +Run once to initialize: +``` +mason get +``` + +After that, `spl_manager add` uses Mason automatically. You can also invoke bricks directly: +``` +mason make feature --name orders +``` + +Brick templates live in `bricks/`. They are excluded from Dart analysis (`analysis_options.yaml`) because they contain Mustache syntax (`{{name.pascalCase()}}`), not valid Dart. + +--- + +## DI Regeneration + +All generated code uses `@injectable` / `@lazySingleton` annotations. After any `add` or `remove` command, `build_runner` is run automatically to regenerate `lib/services/di.config.dart`. + +To run it manually: +``` +dart run codegen/spl_manager.dart fix +``` diff --git a/analysis_options.yaml b/analysis_options.yaml index 61b6c4d..64e8f67 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,6 +9,12 @@ # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - bricks/** # Mason brick templates (Mustache syntax, not valid Dart) + - codegen/** # CLI tools (print statements expected in dev tools) + - features_catalog/** # Inactive features (disabled via spl_manager, code preserved) + linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` diff --git a/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/local/{{name.snakeCase()}}_local_data_sources.dart b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/local/{{name.snakeCase()}}_local_data_sources.dart new file mode 100644 index 0000000..a1c77f3 --- /dev/null +++ b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/local/{{name.snakeCase()}}_local_data_sources.dart @@ -0,0 +1,24 @@ +{{#with_storage}}import 'package:boilerplate/core/storage/app_storage.dart'; +{{/with_storage}}import 'package:injectable/injectable.dart'; + +abstract class {{name.pascalCase()}}LocalDataSources { +{{#with_storage}} Future cache(String key, dynamic value); + Future getCached(String key); + Future clearCache(); +{{/with_storage}}} + +@LazySingleton(as: {{name.pascalCase()}}LocalDataSources) +class {{name.pascalCase()}}LocalDataSourcesImpl implements {{name.pascalCase()}}LocalDataSources { +{{#with_storage}} final AppStorage _storage; + const {{name.pascalCase()}}LocalDataSourcesImpl(this._storage); + + @override + Future cache(String key, dynamic value) => _storage.put(key, value); + + @override + Future getCached(String key) => _storage.get(key); + + @override + Future clearCache() => _storage.clear(); +{{/with_storage}}{{^with_storage}} const {{name.pascalCase()}}LocalDataSourcesImpl(); +{{/with_storage}}} diff --git a/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/model/mapper/{{name.snakeCase()}}_mapper.dart b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/model/mapper/{{name.snakeCase()}}_mapper.dart new file mode 100644 index 0000000..2a86cea --- /dev/null +++ b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/model/mapper/{{name.snakeCase()}}_mapper.dart @@ -0,0 +1,8 @@ +import '../responses/{{name.snakeCase()}}_response.dart'; +import '../../../domain/model/{{name.snakeCase()}}.dart'; + +class {{name.pascalCase()}}Mapper { + static {{name.pascalCase()}} mapResponseToDomain({{name.pascalCase()}}Response response) { + return {{name.pascalCase()}}(id: response.id); + } +} diff --git a/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/model/responses/{{name.snakeCase()}}_response.dart b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/model/responses/{{name.snakeCase()}}_response.dart new file mode 100644 index 0000000..3c54672 --- /dev/null +++ b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/model/responses/{{name.snakeCase()}}_response.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part '{{name.snakeCase()}}_response.freezed.dart'; +part '{{name.snakeCase()}}_response.g.dart'; + +@freezed +abstract class {{name.pascalCase()}}Response with _${{name.pascalCase()}}Response { + const factory {{name.pascalCase()}}Response({ + required int id, + }) = _{{name.pascalCase()}}Response; + + factory {{name.pascalCase()}}Response.fromJson(Map json) => + _${{name.pascalCase()}}ResponseFromJson(json); +} diff --git a/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/remote/{{name.snakeCase()}}_remote_data_sources.dart b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/remote/{{name.snakeCase()}}_remote_data_sources.dart new file mode 100644 index 0000000..136d2c5 --- /dev/null +++ b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/remote/{{name.snakeCase()}}_remote_data_sources.dart @@ -0,0 +1,20 @@ +import 'package:boilerplate/core/client/network_service.dart'; +import 'package:injectable/injectable.dart'; + +import '../model/responses/{{name.snakeCase()}}_response.dart'; + +abstract class {{name.pascalCase()}}RemoteDataSources { + Future<{{name.pascalCase()}}Response> getSomething(); +} + +@LazySingleton(as: {{name.pascalCase()}}RemoteDataSources) +class {{name.pascalCase()}}RemoteDataSourceImpl implements {{name.pascalCase()}}RemoteDataSources { + final NetworkService _networkService; + const {{name.pascalCase()}}RemoteDataSourceImpl(this._networkService); + + @override + Future<{{name.pascalCase()}}Response> getSomething() async { + // TODO: implement using _networkService + throw UnimplementedError(); + } +} diff --git a/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/{{name.snakeCase()}}_repository_impl.dart b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/{{name.snakeCase()}}_repository_impl.dart new file mode 100644 index 0000000..c74d8d7 --- /dev/null +++ b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/data/{{name.snakeCase()}}_repository_impl.dart @@ -0,0 +1,26 @@ +import 'package:boilerplate/core/client/api_call.dart'; +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +import 'local/{{name.snakeCase()}}_local_data_sources.dart'; +import 'model/mapper/{{name.snakeCase()}}_mapper.dart'; +import 'remote/{{name.snakeCase()}}_remote_data_sources.dart'; +import '../domain/model/{{name.snakeCase()}}.dart'; +import '../domain/repository/{{name.snakeCase()}}_repository.dart'; + +@LazySingleton(as: {{name.pascalCase()}}Repository) +class {{name.pascalCase()}}RepositoryImpl implements {{name.pascalCase()}}Repository { + final {{name.pascalCase()}}RemoteDataSources _remote; + final {{name.pascalCase()}}LocalDataSources _local; + + const {{name.pascalCase()}}RepositoryImpl(this._remote, this._local); + + @override + Future> getSomething() { + return apiCall<{{name.pascalCase()}}>( + func: _remote.getSomething(), + mapper: (value) => {{name.pascalCase()}}Mapper.mapResponseToDomain(value), + ); + } +} diff --git a/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/model/{{name.snakeCase()}}.dart b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/model/{{name.snakeCase()}}.dart new file mode 100644 index 0000000..39e8cfa --- /dev/null +++ b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/model/{{name.snakeCase()}}.dart @@ -0,0 +1,4 @@ +class {{name.pascalCase()}} { + final int id; + const {{name.pascalCase()}}({required this.id}); +} diff --git a/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/repository/{{name.snakeCase()}}_repository.dart b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/repository/{{name.snakeCase()}}_repository.dart new file mode 100644 index 0000000..d5edfb6 --- /dev/null +++ b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/repository/{{name.snakeCase()}}_repository.dart @@ -0,0 +1,8 @@ +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:dartz/dartz.dart'; + +import '../model/{{name.snakeCase()}}.dart'; + +abstract class {{name.pascalCase()}}Repository { + Future> getSomething(); +} diff --git a/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/use_cases/{{name.snakeCase()}}_use_cases.dart b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/use_cases/{{name.snakeCase()}}_use_cases.dart new file mode 100644 index 0000000..65ddf65 --- /dev/null +++ b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/use_cases/{{name.snakeCase()}}_use_cases.dart @@ -0,0 +1,8 @@ +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:dartz/dartz.dart'; + +import '../model/{{name.snakeCase()}}.dart'; + +abstract class {{name.pascalCase()}}UseCases { + Future> getSomething(); +} diff --git a/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/{{name.snakeCase()}}_interactor.dart b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/{{name.snakeCase()}}_interactor.dart new file mode 100644 index 0000000..4922ec6 --- /dev/null +++ b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/domain/{{name.snakeCase()}}_interactor.dart @@ -0,0 +1,16 @@ +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +import 'repository/{{name.snakeCase()}}_repository.dart'; +import 'use_cases/{{name.snakeCase()}}_use_cases.dart'; + +@LazySingleton(as: {{name.pascalCase()}}UseCases) +class {{name.pascalCase()}}Interactor implements {{name.pascalCase()}}UseCases { + final {{name.pascalCase()}}Repository _repository; + const {{name.pascalCase()}}Interactor(this._repository); + + @override + Future> getSomething() => + _repository.getSomething(); +} diff --git a/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/blocs/{{name.snakeCase()}}_bloc.dart b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/blocs/{{name.snakeCase()}}_bloc.dart new file mode 100644 index 0000000..40dbd4e --- /dev/null +++ b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/blocs/{{name.snakeCase()}}_bloc.dart @@ -0,0 +1,27 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; + +import '../../domain/use_cases/{{name.snakeCase()}}_use_cases.dart'; +import '{{name.snakeCase()}}_event.dart'; +import '{{name.snakeCase()}}_state.dart'; + +@Injectable() +class {{name.pascalCase()}}Bloc extends Bloc<{{name.pascalCase()}}Event, {{name.pascalCase()}}State> { + final {{name.pascalCase()}}UseCases _useCases; + + {{name.pascalCase()}}Bloc(this._useCases) : super(const {{name.pascalCase()}}InitialState()) { + on(_onGet); + } + + Future _onGet( + Get{{name.pascalCase()}}Event event, + Emitter<{{name.pascalCase()}}State> emit, + ) async { + emit(const {{name.pascalCase()}}LoadingState()); + final result = await _useCases.getSomething(); + result.fold( + (failure) => emit({{name.pascalCase()}}ErrorState(message: failure.message ?? '')), + (data) => emit({{name.pascalCase()}}SuccessState(data: data)), + ); + } +} diff --git a/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/blocs/{{name.snakeCase()}}_event.dart b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/blocs/{{name.snakeCase()}}_event.dart new file mode 100644 index 0000000..c11e189 --- /dev/null +++ b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/blocs/{{name.snakeCase()}}_event.dart @@ -0,0 +1,12 @@ +import 'package:equatable/equatable.dart'; + +abstract class {{name.pascalCase()}}Event extends Equatable { + const {{name.pascalCase()}}Event(); + + @override + List get props => []; +} + +class Get{{name.pascalCase()}}Event extends {{name.pascalCase()}}Event { + const Get{{name.pascalCase()}}Event(); +} diff --git a/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/blocs/{{name.snakeCase()}}_state.dart b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/blocs/{{name.snakeCase()}}_state.dart new file mode 100644 index 0000000..eaea6df --- /dev/null +++ b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/blocs/{{name.snakeCase()}}_state.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; + +abstract class {{name.pascalCase()}}State extends Equatable { + const {{name.pascalCase()}}State(); + + @override + List get props => []; +} + +class {{name.pascalCase()}}InitialState extends {{name.pascalCase()}}State { + const {{name.pascalCase()}}InitialState(); +} + +class {{name.pascalCase()}}LoadingState extends {{name.pascalCase()}}State { + const {{name.pascalCase()}}LoadingState(); +} + +class {{name.pascalCase()}}SuccessState extends {{name.pascalCase()}}State { + final dynamic data; + const {{name.pascalCase()}}SuccessState({required this.data}); + + @override + List get props => [data]; +} + +class {{name.pascalCase()}}ErrorState extends {{name.pascalCase()}}State { + final String message; + const {{name.pascalCase()}}ErrorState({required this.message}); + + @override + List get props => [message]; +} diff --git a/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/pages/{{name.snakeCase()}}_page.dart b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/pages/{{name.snakeCase()}}_page.dart new file mode 100644 index 0000000..7827d14 --- /dev/null +++ b/bricks/feature/__brick__/lib/features/{{name.snakeCase()}}/presentation/pages/{{name.snakeCase()}}_page.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class {{name.pascalCase()}}Page extends StatelessWidget { + static const route = '/{{name.snakeCase()}}'; + const {{name.pascalCase()}}Page({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('{{name.pascalCase()}}')), + body: const Center(child: Text('{{name.pascalCase()}} — replace me')), + ); + } +} diff --git a/bricks/feature/brick.yaml b/bricks/feature/brick.yaml new file mode 100644 index 0000000..45306ab --- /dev/null +++ b/bricks/feature/brick.yaml @@ -0,0 +1,15 @@ +name: feature +description: Scaffolds a clean architecture feature module with injectable DI +version: 0.1.0+1 +environment: + mason: ">=0.1.0 <0.2.0" +vars: + name: + type: string + description: Feature name (snake_case, e.g. orders or user_profile) + prompt: "Feature name (snake_case)" + with_storage: + type: boolean + description: Include AppStorage-backed local data cache? + default: false + prompt: "Include local storage cache?" diff --git a/bricks/storage_hive/__brick__/lib/core/storage/impl/hive_storage_provider.dart b/bricks/storage_hive/__brick__/lib/core/storage/impl/hive_storage_provider.dart new file mode 100644 index 0000000..6c162ed --- /dev/null +++ b/bricks/storage_hive/__brick__/lib/core/storage/impl/hive_storage_provider.dart @@ -0,0 +1,32 @@ +import 'package:hive_flutter/hive_flutter.dart'; +import '../app_storage.dart'; + +/// [AppStorage] backed by Hive (fast NoSQL box store). +/// Managed by spl_manager. To switch: dart run codegen/spl_manager.dart storage set +/// Requires: hive_flutter: ^1.1.0 in pubspec.yaml +/// Call di().init() in main() before runApp(). +class HiveStorageProvider implements AppStorage { + late Box _box; + static const _boxName = 'app_storage'; + + @override + Future init() async { + await Hive.initFlutter(); + _box = await Hive.openBox(_boxName); + } + + @override + Future put(String key, dynamic value) async => _box.put(key, value); + + @override + Future get(String key) async => _box.get(key) as T?; + + @override + Future delete(String key) async => _box.delete(key); + + @override + Future clear() async => _box.clear(); + + @override + Future contains(String key) async => _box.containsKey(key); +} diff --git a/bricks/storage_hive/brick.yaml b/bricks/storage_hive/brick.yaml new file mode 100644 index 0000000..d6998f5 --- /dev/null +++ b/bricks/storage_hive/brick.yaml @@ -0,0 +1,6 @@ +name: storage_hive +description: Generates the Hive AppStorage provider (requires hive_flutter in pubspec) +version: 0.1.0+1 +environment: + mason: ">=0.1.0 <0.2.0" +vars: {} diff --git a/bricks/storage_prefs/__brick__/lib/core/storage/impl/shared_prefs_storage_provider.dart b/bricks/storage_prefs/__brick__/lib/core/storage/impl/shared_prefs_storage_provider.dart new file mode 100644 index 0000000..ae9ebc1 --- /dev/null +++ b/bricks/storage_prefs/__brick__/lib/core/storage/impl/shared_prefs_storage_provider.dart @@ -0,0 +1,40 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import '../app_storage.dart'; + +/// [AppStorage] backed by SharedPreferences (simple non-encrypted key-value). +/// Managed by spl_manager. To switch: dart run codegen/spl_manager.dart storage set +/// Requires: shared_preferences: ^2.3.0 in pubspec.yaml +/// Call di().init() in main() before runApp(). +class SharedPrefsStorageProvider implements AppStorage { + late SharedPreferences _prefs; + + @override + Future init() async { + _prefs = await SharedPreferences.getInstance(); + } + + @override + Future put(String key, dynamic value) async { + if (value is int) { + await _prefs.setInt(key, value); + } else if (value is double) { + await _prefs.setDouble(key, value); + } else if (value is bool) { + await _prefs.setBool(key, value); + } else { + await _prefs.setString(key, value.toString()); + } + } + + @override + Future get(String key) async => _prefs.get(key) as T?; + + @override + Future delete(String key) async => _prefs.remove(key); + + @override + Future clear() async => _prefs.clear(); + + @override + Future contains(String key) async => _prefs.containsKey(key); +} diff --git a/bricks/storage_prefs/brick.yaml b/bricks/storage_prefs/brick.yaml new file mode 100644 index 0000000..3ea21a5 --- /dev/null +++ b/bricks/storage_prefs/brick.yaml @@ -0,0 +1,6 @@ +name: storage_prefs +description: Generates the SharedPreferences AppStorage provider (requires shared_preferences in pubspec) +version: 0.1.0+1 +environment: + mason: ">=0.1.0 <0.2.0" +vars: {} diff --git a/bricks/storage_secure/__brick__/lib/core/storage/impl/secure_storage_provider.dart b/bricks/storage_secure/__brick__/lib/core/storage/impl/secure_storage_provider.dart new file mode 100644 index 0000000..7aa970f --- /dev/null +++ b/bricks/storage_secure/__brick__/lib/core/storage/impl/secure_storage_provider.dart @@ -0,0 +1,36 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import '../app_storage.dart'; + +/// [AppStorage] backed by FlutterSecureStorage (encrypted on-device key-value). +/// Managed by spl_manager. To switch: dart run codegen/spl_manager.dart storage set +class SecureStorageProvider implements AppStorage { + final FlutterSecureStorage _storage; + const SecureStorageProvider(this._storage); + + @override + Future init() async {} + + @override + Future put(String key, dynamic value) async { + await _storage.write(key: key, value: value.toString()); + } + + @override + Future get(String key) async { + final value = await _storage.read(key: key); + if (value == null) return null; + if (T == int) return int.tryParse(value) as T?; + if (T == double) return double.tryParse(value) as T?; + if (T == bool) return (value == 'true') as T?; + return value as T?; + } + + @override + Future delete(String key) async => _storage.delete(key: key); + + @override + Future clear() async => _storage.deleteAll(); + + @override + Future contains(String key) async => _storage.containsKey(key: key); +} diff --git a/bricks/storage_secure/brick.yaml b/bricks/storage_secure/brick.yaml new file mode 100644 index 0000000..5f63e89 --- /dev/null +++ b/bricks/storage_secure/brick.yaml @@ -0,0 +1,6 @@ +name: storage_secure +description: Generates the FlutterSecureStorage AppStorage provider +version: 0.1.0+1 +environment: + mason: ">=0.1.0 <0.2.0" +vars: {} diff --git a/bricks/storage_sqflite/__brick__/lib/core/storage/impl/sqflite_storage_provider.dart b/bricks/storage_sqflite/__brick__/lib/core/storage/impl/sqflite_storage_provider.dart new file mode 100644 index 0000000..10121a3 --- /dev/null +++ b/bricks/storage_sqflite/__brick__/lib/core/storage/impl/sqflite_storage_provider.dart @@ -0,0 +1,56 @@ +import 'package:sqflite/sqflite.dart'; +import '../app_storage.dart'; + +/// [AppStorage] backed by sqflite (SQLite relational DB). +/// Managed by spl_manager. To switch: dart run codegen/spl_manager.dart storage set +class SqfliteStorageProvider implements AppStorage { + Database? _db; + static const _table = 'kv_store'; + + @override + Future init() async { + final path = await getDatabasesPath(); + _db = await openDatabase( + '$path/app_storage.db', + version: 1, + onCreate: (db, _) async { + await db.execute( + 'CREATE TABLE $_table (key TEXT PRIMARY KEY, value TEXT NOT NULL)', + ); + }, + ); + } + + @override + Future put(String key, dynamic value) async { + await _db!.insert( + _table, + {'key': key, 'value': value.toString()}, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + @override + Future get(String key) async { + final rows = await _db!.query(_table, where: 'key = ?', whereArgs: [key]); + if (rows.isEmpty) return null; + final raw = rows.first['value'] as String; + if (T == int) return int.tryParse(raw) as T?; + if (T == double) return double.tryParse(raw) as T?; + if (T == bool) return (raw == 'true') as T?; + return raw as T?; + } + + @override + Future delete(String key) async => + _db!.delete(_table, where: 'key = ?', whereArgs: [key]); + + @override + Future clear() async => _db!.delete(_table); + + @override + Future contains(String key) async { + final rows = await _db!.query(_table, where: 'key = ?', whereArgs: [key]); + return rows.isNotEmpty; + } +} diff --git a/bricks/storage_sqflite/brick.yaml b/bricks/storage_sqflite/brick.yaml new file mode 100644 index 0000000..5b25334 --- /dev/null +++ b/bricks/storage_sqflite/brick.yaml @@ -0,0 +1,6 @@ +name: storage_sqflite +description: Generates the sqflite AppStorage provider +version: 0.1.0+1 +environment: + mason: ">=0.1.0 <0.2.0" +vars: {} diff --git a/codegen/module_generator.dart b/codegen/module_generator.dart deleted file mode 100644 index b6709f1..0000000 --- a/codegen/module_generator.dart +++ /dev/null @@ -1,422 +0,0 @@ -import 'dart:io'; - -void main() async { - print("Enter module name: ... (example: profile) "); - // Reading name of the Geek - String input = await stdin.readLineSync() ?? ""; - String module = input.toLowerCase(); - - // Define the list of folders and files - List folders = [ - 'lib/features/$module/data/local', - 'lib/features/$module/data/model/mapper', - 'lib/features/$module/data/model/responses', - 'lib/features/$module/data/remote', - 'lib/features/$module/di', - 'lib/features/$module/domain', - 'lib/features/$module/domain/model', - 'lib/features/$module/domain/repository', - 'lib/features/$module/domain/use_cases', - 'lib/features/$module/presentation/blocs/events', - 'lib/features/$module/presentation/blocs/states', - 'lib/features/$module/presentation/blocs', - 'lib/features/$module/presentation/pages', - 'lib/features/$module/presentation/widgets', - ]; - - List files = [ - 'lib/features/$module/data/local/${module}_local_data_sources.dart', - 'lib/features/$module/data/model/mapper/${module}_mapper.dart', - 'lib/features/$module/data/model/responses/${module}_response.dart', - 'lib/features/$module/data/remote/${module}_remote_data_sources.dart', - 'lib/features/$module/data/${module}_repository_impl.dart', - 'lib/features/$module/di/${module}_module.dart', - 'lib/features/$module/domain/model/$module.dart', - 'lib/features/$module/domain/repository/${module}_repository.dart', - 'lib/features/$module/domain/use_cases/${module}_use_cases.dart', - 'lib/features/$module/domain/${module}_interactor.dart', - 'lib/features/$module/presentation/blocs/events/get_${module}_event.dart', - 'lib/features/$module/presentation/blocs/states/get_${module}_states.dart', - 'lib/features/$module/presentation/blocs/${module}_bloc.dart', - 'lib/features/$module/presentation/blocs/${module}_events.dart', - 'lib/features/$module/presentation/blocs/${module}_states.dart', - 'lib/features/$module/presentation/pages/${module}_page.dart', - 'lib/features/$module/presentation/widgets/${module}_info_widget.dart', - ]; - - // Create folders - for (String folder in folders) { - createFolder(folder); - } - - // Create files - for (String file in files) { - File plainFile = await createFile(file); - writeFile(plainFile, file, module, files); - } - - print('Folders and files created successfully!'); -} - -void createFolder(String folderPath) { - Directory(folderPath).create(recursive: true).then((Directory directory) { - print('Folder created: ${directory.path}'); - }); -} - -Future createFile(String filePath) async { - return await File(filePath).create(recursive: true); -} - -void writeFile(File file, String fileName, String module, List files) { - String content = getContent(fileName, module, files); - - file.writeAsString(content, mode: FileMode.append).then((File file) { - print('File name written to ${file.path}'); - }); -} - -String getContent(String fileName, String module, List files) { - final className = toUpperCaseFirst(module); - if (fileName == files[0]) { - return writeLocalDataSource(className); - } else if (fileName == files[1]) { - return writeMapper(className); - } else if (fileName == files[2]) { - return writeResponse(className); - } else if (fileName == files[3]) { - return writeRemoteDataSource(className); - } else if (fileName == files[4]) { - return writeRepositoryImpl(className); - } else if (fileName == files[5]) { - return writeModule(className); - } else if (fileName == files[6]) { - return writeModel(className); - } else if (fileName == files[7]) { - return writeRepository(className); - } else if (fileName == files[8]) { - return writeUseCases(className); - } else if (fileName == files[9]) { - return writeInteractor(className); - } else if (fileName == files[10]) { - return writeGetModuleEvent(className); - } else if (fileName == files[11]) { - return writeGetModuleStates(className); - } else if (fileName == files[12]) { - return writeBloc(className); - } else if (fileName == files[13]) { - return writeEvents(className); - } else if (fileName == files[14]) { - return writeStates(className); - } else if (fileName == files[15]) { - return writePage(className); - } else if (fileName == files[16]) { - return writeInfoWidget(className); - } - return fileName; -} - -String toUpperCaseFirst(String value) { - StringBuffer buffer = StringBuffer(); - buffer.write(value[0].toUpperCase()); - buffer.write(value.substring(1)); - return buffer.toString(); -} - -String writeLocalDataSource(String module) { - return ''' -abstract class ${module}LocalDataSources {} - -class ${module}LocalDataSourcesImpl implements ${module}LocalDataSources { - ${module}LocalDataSourcesImpl(); -} -'''; -} - -String writeMapper(String module) { - return ''' -class ${module}Mapper { - static void mapResponseToDomain() { - return; - } -} - -'''; -} - -String writeResponse(String module) { - return ''' -import 'package:freezed_annotation/freezed_annotation.dart'; - -part '${module.toLowerCase()}_response.freezed.dart'; -part '${module.toLowerCase()}_response.g.dart'; - -@freezed -class ${module}Response with _\$${module}Response { - const factory ${module}Response({ - required int id, - required String ${module}name, - required String email, - }) = _${module}Response; - - factory ${module}Response.fromJson(Map json) => - _\$${module}ResponseFromJson(json); -} -'''; -} - -String writeRemoteDataSource(String module) { - return ''' -import 'package:boilerplate/core/client/network_service.dart'; - -abstract class ${module}RemoteDataSources { - Future getSomething(); -} - -class ${module}RemoteDataSourceImpl implements ${module}RemoteDataSources { - final NetworkService networkService; - - const ${module}RemoteDataSourceImpl(this.networkService); - - @override - Future getSomething() async { - return; - } -} - -'''; -} - -String writeRepositoryImpl(String module) { - return ''' -import 'package:boilerplate/core/client/network_exception.dart'; -import 'package:boilerplate/features/${module.toLowerCase()}/data/local/${module.toLowerCase()}_local_data_sources.dart'; -import 'package:boilerplate/features/${module.toLowerCase()}/data/remote/${module.toLowerCase()}_remote_data_sources.dart'; -import 'package:dartz/dartz.dart'; - -import '../../../core/client/api_call.dart'; -import '../domain/repository/${module.toLowerCase()}_repository.dart'; -import 'model/mapper/${module.toLowerCase()}_mapper.dart'; - -class ${module}RepositoryImpl implements ${module}Repository { - final ${module}RemoteDataSources _remoteDataSources; - final ${module}LocalDataSources _localDataSources; - - const ${module}RepositoryImpl(this._remoteDataSources, this._localDataSources); - - @override - Future> getSomething() { - return apiCall( - func: _remoteDataSources.getSomething(), - mapper: (_) => ${module}Mapper.mapResponseToDomain(), - ); - } -} -'''; -} - -String writeModule(String module) { - return ''' -import '/core/client/network_service.dart'; -import '/features/${module.toLowerCase()}/data/local/${module.toLowerCase()}_local_data_sources.dart'; -import '/features/${module.toLowerCase()}/data/${module.toLowerCase()}_repository_impl.dart'; -import '/features/${module.toLowerCase()}/data/remote/${module.toLowerCase()}_remote_data_sources.dart'; -import '/features/${module.toLowerCase()}/domain/${module.toLowerCase()}_interactor.dart'; -import '/features/${module.toLowerCase()}/domain/repository/${module.toLowerCase()}_repository.dart'; -import '/features/${module.toLowerCase()}/domain/use_cases/${module.toLowerCase()}_use_cases.dart'; -import '/features/${module.toLowerCase()}/presentation/blocs/${module.toLowerCase()}_bloc.dart'; -import 'package:get_it/get_it.dart'; - -void register$module(GetIt di) { - di.registerFactory<${module}RemoteDataSources>( - () => ${module}RemoteDataSourceImpl(di())); - di.registerFactory<${module}LocalDataSources>( - () => ${module}LocalDataSourcesImpl()); - di.registerFactory<${module}Repository>(() => ${module}RepositoryImpl( - di<${module}RemoteDataSources>(), di<${module}LocalDataSources>())); - di.registerFactory<${module}UseCases>( - () => ${module}Interactor(di<${module}Repository>())); - di.registerLazySingleton<${module}Bloc>( - () => ${module}Bloc(di<${module}UseCases>())); -} -'''; -} - -String writeRepository(String module) { - return ''' -import 'package:boilerplate/core/client/network_exception.dart'; -import 'package:dartz/dartz.dart'; - -abstract class ${module}Repository { - Future> getSomething(); -} -'''; -} - -String writeModel(String module) { - return ''' -class $module { - final int id; - final String username; - - const $module({ - required this.id, - required this.username, - }); -} -'''; -} - -String writeUseCases(String module) { - return ''' -import 'package:boilerplate/core/client/network_exception.dart'; -import 'package:dartz/dartz.dart'; - -abstract class ${module}UseCases { - Future> getSomething(); -} -'''; -} - -String writeInteractor(String module) { - return ''' -import 'package:boilerplate/core/client/network_exception.dart'; -import 'package:boilerplate/features/${module.toLowerCase()}/domain/repository/${module.toLowerCase()}_repository.dart'; -import 'package:boilerplate/features/${module.toLowerCase()}/domain/use_cases/${module.toLowerCase()}_use_cases.dart'; -import 'package:dartz/dartz.dart'; - -class ${module}Interactor implements ${module}UseCases { - final ${module}Repository _repository; - - const ${module}Interactor(this._repository); - - @override - Future> getSomething() { - return _repository.getSomething(); - } -} -'''; -} - -String writeGetModuleEvent(String module) { - return ''' -import '../${module.toLowerCase()}_events.dart'; - -class Get${module}Event extends ${module}Event {} -'''; -} - -String writeGetModuleStates(String module) { - return ''' -import '../${module.toLowerCase()}_states.dart'; - -class Get${module}InitState extends ${module}States { - @override - List get props => []; -} - -class Get${module}LoadingState extends ${module}States { - @override - List get props => []; -} - -class Get${module}SuccessState extends ${module}States { - final List items; - - Get${module}SuccessState({required this.items}); - - @override - List get props => [items]; -} - -class Get${module}ErrorState extends ${module}States { - final String message; - - Get${module}ErrorState({required this.message}); - - @override - List get props => [message]; -} - -'''; -} - -String writeBloc(String module) { - return ''' -import 'package:boilerplate/features/${module.toLowerCase()}/presentation/blocs/states/get_${module.toLowerCase()}_states.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../domain/use_cases/${module.toLowerCase()}_use_cases.dart'; -import 'events/get_${module.toLowerCase()}_event.dart'; -import '${module.toLowerCase()}_events.dart'; -import '${module.toLowerCase()}_states.dart'; - -class ${module}Bloc extends Bloc<${module}Event, ${module}States> { - final ${module}UseCases _useCases; - - ${module}Bloc(this._useCases) : super(Get${module}InitState()) { - on(_onGet${module}Event); - } - - Future _onGet${module}Event( - Get${module}Event event, Emitter<${module}States> emitter) async { - emitter(Get${module}InitState()); - final response = await _useCases.getSomething(); - await response.fold( - (l) { - emitter( - Get${module}ErrorState(message: l.message ?? ''), - ); - }, - (r) async { - emitter(Get${module}SuccessState(items: [''])); - }, - ); - } -} -'''; -} - -String writeEvents(String module) { - return ''' -abstract class ${module}Event {} -'''; -} - -String writeStates(String module) { - return ''' -import 'package:equatable/equatable.dart'; - -abstract class ${module}States extends Equatable {} -'''; -} - -String writePage(String module) { - return ''' -import 'package:flutter/material.dart'; - -class ${module}Page extends StatelessWidget { - const ${module}Page({super.key}); - - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} -'''; -} - -String writeInfoWidget(String module) { - return ''' -import 'package:flutter/material.dart'; - -class ${module}InfoWidget extends StatelessWidget { - const ${module}InfoWidget({super.key}); - - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} -'''; -} diff --git a/codegen/spl_manager.dart b/codegen/spl_manager.dart new file mode 100644 index 0000000..a033564 --- /dev/null +++ b/codegen/spl_manager.dart @@ -0,0 +1,1278 @@ +// ignore_for_file: avoid_print +/// SPL Manager — Software Product Line CLI for Flutter Clean Architecture +/// +/// Variability points: +/// Storage (XOR) — one backend for the whole app +/// State Mgmt (OR) — global default, per-feature override allowed +/// +/// Usage: +/// dart run codegen/spl_manager.dart [args] +/// +/// Commands: +/// list +/// add [--with-storage] [--state bloc|cubit|riverpod] +/// disable Move feature to catalog (keeps code, unwires DI) +/// enable Restore feature from catalog (wires DI) +/// remove [--yes|-y] Hard delete (works on active or catalog features) +/// storage set flutter_secure_storage|sqflite|hive|shared_preferences +/// storage list +/// state set bloc|cubit|riverpod +/// state list +/// fix +library; + +import 'dart:io'; + +// ─── Entry point ────────────────────────────────────────────────────────────── + +void main(List args) async { + if (args.isEmpty) { _printHelp(); exit(0); } + + switch (args[0]) { + case 'list': + await _cmdList(); + case 'add': + if (args.length < 2) _die('Usage: add [--with-storage] [--state bloc|cubit|riverpod]'); + final withStorage = args.contains('--with-storage'); + final stateIdx = args.indexOf('--state'); + final stateOverride = stateIdx != -1 && stateIdx + 1 < args.length + ? args[stateIdx + 1] + : null; + await _cmdAdd(args[1], withStorage: withStorage, stateOverride: stateOverride); + case 'disable': + if (args.length < 2) _die('Usage: disable '); + await _cmdDisable(args[1]); + case 'enable': + if (args.length < 2) _die('Usage: enable '); + await _cmdEnable(args[1]); + case 'remove': + if (args.length < 2) _die('Usage: remove [--yes|-y]'); + final force = args.contains('--yes') || args.contains('-y'); + await _cmdRemove(args[1], force: force); + case 'storage': + if (args.length < 2) _die('Usage: storage set | storage list'); + if (args[1] == 'set') { + if (args.length < 3) _die('Usage: storage set '); + await _cmdStorageSet(args[2]); + } else if (args[1] == 'list') { + _cmdStorageList(); + } else { + _die('Unknown storage subcommand: ${args[1]}'); + } + case 'state': + if (args.length < 2) _die('Usage: state set | state list'); + if (args[1] == 'set') { + if (args.length < 3) _die('Usage: state set '); + _cmdStateSet(args[2]); + } else if (args[1] == 'list') { + _cmdStateList(); + } else { + _die('Unknown state subcommand: ${args[1]}'); + } + case 'fix': + await _cmdFix(); + default: + _die('Unknown command: ${args[0]}'); + } +} + +// ─── Commands ───────────────────────────────────────────────────────────────── + +Future _cmdList() async { + final config = _readSplConfig(); + _printHeader('SPL Configuration'); + + final storage = config['storage']?['local_backend'] ?? 'flutter_secure_storage'; + final stateDefault = config['state_management']?['default'] ?? 'bloc'; + + print(' App : ${config['app']?['name'] ?? 'unknown'}'); + print(' Storage [XOR] : $storage'); + print(' State Mgmt [OR] : $stateDefault (default, per-feature override allowed)'); + print(''); + + final features = config['features'] as List>? ?? []; + if (features.isEmpty) { + print(' No features yet.'); + print(' dart run codegen/spl_manager.dart add '); + return; + } + + final active = features.where((f) => (f['status'] ?? 'active') == 'active').toList(); + final inactive = features.where((f) => (f['status'] ?? 'active') == 'inactive').toList(); + + print(' Active features [compiled + DI-wired]:'); + if (active.isEmpty) { + print(' (none)'); + } else { + for (final f in active) { + final name = f['name'] ?? '?'; + final storage = f['storage'] ?? 'none'; + final state = f['state'] ?? stateDefault; + final desc = f['description'] ?? ''; + final storageTag = storage == 'none' ? '' : ' storage:$storage'; + print(' ✓ $name state:$state$storageTag'); + if (desc.isNotEmpty && desc != '""') print(' $desc'); + } + } + + if (inactive.isNotEmpty) { + print(''); + print(' Catalog [code preserved, not compiled]:'); + for (final f in inactive) { + final name = f['name'] ?? '?'; + final storage = f['storage'] ?? 'none'; + final state = f['state'] ?? stateDefault; + final desc = f['description'] ?? ''; + final storageTag = storage == 'none' ? '' : ' storage:$storage'; + print(' ○ $name state:$state$storageTag'); + if (desc.isNotEmpty && desc != '""') print(' $desc'); + } + } + + print(''); + print(' Tip: dart run codegen/spl_manager.dart storage list'); + print(' dart run codegen/spl_manager.dart state list'); +} + +Future _cmdAdd( + String name, { + bool withStorage = false, + String? stateOverride, +}) async { + final module = name.toLowerCase().replaceAll(RegExp(r'[^a-z0-9_]'), '_'); + final className = _toPascalCase(module); + final featureDir = 'lib/features/$module'; + + if (Directory(featureDir).existsSync()) { + _die('Feature "$module" already exists at $featureDir'); + } + if (Directory('features_catalog/$module').existsSync()) { + _die('Feature "$module" exists in the catalog (disabled).\n' + ' To restore it: dart run codegen/spl_manager.dart enable $module\n' + ' To delete it: dart run codegen/spl_manager.dart remove $module'); + } + + final config = _readSplConfig(); + final globalStorageBackend = config['storage']?['local_backend'] ?? 'flutter_secure_storage'; + final globalStateDefault = config['state_management']?['default'] ?? 'bloc'; + final stateChoice = stateOverride ?? globalStateDefault; + + _validateStateChoice(stateChoice); + + _printHeader('Adding feature: $module'); + print(' Class : $className'); + print(' Storage : ${withStorage ? globalStorageBackend : 'none'}'); + print(' State : $stateChoice${stateOverride != null ? ' (override)' : ' (default)'}'); + print(' DI : auto-wired via build_runner (@injectable)'); + print(''); + + final usedMason = await _tryMasonFeature(module, + withStorage: withStorage, state: stateChoice); + if (!usedMason) { + _generateFeatureFiles(module, className, + withStorage: withStorage, state: stateChoice); + } + + _addFeatureToConfig( + module, + storage: withStorage ? globalStorageBackend : 'none', + state: stateChoice, + ); + + _printStateNotes(stateChoice); + + print('\n Wiring DI (build_runner)...'); + await _runBuildRunner(); + + print('\n ✓ Done! lib/features/$module/'); + print(' → Register the route in lib/core/router/app_router_config.dart'); +} + +Future _cmdDisable(String name) async { + final module = name.toLowerCase(); + final activeDir = 'lib/features/$module'; + final catalogDir = 'features_catalog/$module'; + + _printHeader('Disabling feature: $module'); + + if (!Directory(activeDir).existsSync()) { + if (Directory(catalogDir).existsSync()) { + _die('Feature "$module" is already disabled (in catalog).'); + } + _die('Feature "$module" not found.'); + } + + Directory('features_catalog').createSync(); + Directory(activeDir).renameSync(catalogDir); + print(' ○ Moved: $activeDir → $catalogDir'); + + _updateFeatureStatusInConfig(module, 'inactive'); + + print(' Regenerating DI...'); + await _runBuildRunner(); + print('\n ✓ Feature "$module" disabled.'); + print(' → Restore with: dart run codegen/spl_manager.dart enable $module'); +} + +Future _cmdEnable(String name) async { + final module = name.toLowerCase(); + final activeDir = 'lib/features/$module'; + final catalogDir = 'features_catalog/$module'; + + _printHeader('Enabling feature: $module'); + + if (!Directory(catalogDir).existsSync()) { + if (Directory(activeDir).existsSync()) { + _die('Feature "$module" is already active.'); + } + _die('Feature "$module" not found in catalog.\n' + ' Add it fresh: dart run codegen/spl_manager.dart add $module'); + } + + Directory('lib/features').createSync(recursive: true); + Directory(catalogDir).renameSync(activeDir); + print(' ✓ Moved: $catalogDir → $activeDir'); + + _updateFeatureStatusInConfig(module, 'active'); + + print(' Wiring DI (build_runner)...'); + await _runBuildRunner(); + print('\n ✓ Feature "$module" enabled.'); + print(' → Ensure route is registered in lib/core/router/app_router_config.dart'); +} + +Future _cmdRemove(String name, {bool force = false}) async { + final module = name.toLowerCase(); + final activeDir = 'lib/features/$module'; + final catalogDir = 'features_catalog/$module'; + + final inActive = Directory(activeDir).existsSync(); + final inCatalog = Directory(catalogDir).existsSync(); + + if (!inActive && !inCatalog) _die('Feature "$module" not found.'); + + final location = inActive ? activeDir : catalogDir; + _printHeader('Removing feature: $module'); + print(' Location: $location${inCatalog ? ' (disabled)' : ' (active)'}'); + + if (!force) { + stdout.write(' Permanently delete "$module"? [y/N] '); + final confirm = stdin.readLineSync()?.toLowerCase(); + if (confirm != 'y' && confirm != 'yes') { print(' Aborted.'); exit(0); } + } + + Directory(location).deleteSync(recursive: true); + print(' Deleted: $location'); + _removeFeatureFromConfig(module); + + if (inActive) { + print(' Regenerating DI...'); + await _runBuildRunner(); + } + print('\n ✓ Feature "$module" permanently removed.'); +} + +Future _cmdStorageSet(String provider) async { + const valid = ['flutter_secure_storage', 'sqflite', 'hive', 'shared_preferences']; + if (!valid.contains(provider)) { + _die('Unknown provider: "$provider"\nValid: ${valid.join(' | ')}'); + } + + final current = _getActiveProviderName(); + if (current == provider) { print('\n Already using "$provider".'); exit(0); } + + _printHeader('Switching storage [XOR]: $current → $provider'); + + _deleteStorageImpl(current); + + final usedMason = await _tryMasonStorage(provider); + if (!usedMason) _generateStorageImpl(provider); + + _rewriteStorageModule(provider); + _updateStorageInConfig(provider); + + print('\n Regenerating DI...'); + await _runBuildRunner(); + + print('\n ✓ Storage → "$provider"'); + _printStorageNotes(provider); +} + +void _cmdStorageList() { + _printHeader('Storage Providers [XOR — exactly one active]'); + final current = _getActiveProviderName(); + final providers = { + 'flutter_secure_storage': 'Encrypted key-value. Strings only. Best for sensitive data.', + 'sqflite': 'SQLite (relational). Best for structured/queryable data.', + 'hive': 'NoSQL box store. Fast reads. Best for object graphs.', + 'shared_preferences': 'Simple key-value. Non-encrypted. Best for user settings.', + }; + for (final e in providers.entries) { + final active = e.key == current ? ' ◀ active' : ''; + print(' ${e.key}$active'); + print(' ${e.value}'); + print(''); + } + print(' Switch (XOR): dart run codegen/spl_manager.dart storage set '); +} + +void _cmdStateSet(String solution) { + _validateStateChoice(solution); + _updateStateDefaultInConfig(solution); + _printHeader('State Management Default → $solution'); + print(' Updated spl.yaml default.'); + print(' Existing features are unchanged.'); + print(' New features will use: $solution'); + _printStateNotes(solution); +} + +void _cmdStateList() { + _printHeader('State Management [OR — global default + per-feature override]'); + final config = _readSplConfig(); + final current = config['state_management']?['default'] ?? 'bloc'; + + final solutions = { + 'bloc': [ + 'flutter_bloc (already in pubspec)', + 'Event + State + Bloc. Explicit event stream. Best for complex flows.', + 'Files: _event.dart _state.dart _bloc.dart', + ], + 'cubit': [ + 'flutter_bloc (already in pubspec, same package as bloc)', + 'State + Cubit only. No event classes. Simpler, fewer files.', + 'Files: _state.dart _cubit.dart', + ], + 'riverpod': [ + 'flutter_riverpod (add to pubspec if not present)', + 'Notifier + Provider. Different DI model. Bridges to get_it via di().', + 'Files: _state.dart _notifier.dart', + ], + }; + + for (final e in solutions.entries) { + final active = e.key == current ? ' ◀ default' : ''; + print(' ${e.key}$active'); + for (final line in e.value) print(' $line'); + print(''); + } + + print(' Change default : dart run codegen/spl_manager.dart state set '); + print(' Per-feature : dart run codegen/spl_manager.dart add --state '); + print(''); + print(' Note: bloc and cubit coexist freely (same package).'); + print(' riverpod requires flutter_riverpod in pubspec.yaml.'); +} + +Future _cmdFix() async { + _printHeader('Running build_runner'); + await _runBuildRunner(); + print(' ✓ Done'); +} + +// ─── Feature file generation ────────────────────────────────────────────────── + +void _generateFeatureFiles( + String module, + String className, { + bool withStorage = false, + String state = 'bloc', +}) { + final dirs = [ + 'lib/features/$module/data/local', + 'lib/features/$module/data/model/mapper', + 'lib/features/$module/data/model/responses', + 'lib/features/$module/data/remote', + 'lib/features/$module/domain/model', + 'lib/features/$module/domain/repository', + 'lib/features/$module/domain/use_cases', + if (state == 'riverpod') + 'lib/features/$module/presentation/providers' + else + 'lib/features/$module/presentation/blocs', + 'lib/features/$module/presentation/pages', + 'lib/features/$module/presentation/widgets', + ]; + for (final d in dirs) Directory(d).createSync(recursive: true); + + final files = { + // Data layer + 'lib/features/$module/data/local/${module}_local_data_sources.dart': + _tplLocalDataSources(module, className, withStorage: withStorage), + 'lib/features/$module/data/model/mapper/${module}_mapper.dart': + _tplMapper(module, className), + 'lib/features/$module/data/model/responses/${module}_response.dart': + _tplResponse(module, className), + 'lib/features/$module/data/remote/${module}_remote_data_sources.dart': + _tplRemoteDataSources(module, className), + 'lib/features/$module/data/${module}_repository_impl.dart': + _tplRepositoryImpl(module, className), + // Domain layer + 'lib/features/$module/domain/model/$module.dart': _tplModel(className), + 'lib/features/$module/domain/repository/${module}_repository.dart': + _tplRepository(module, className), + 'lib/features/$module/domain/use_cases/${module}_use_cases.dart': + _tplUseCases(module, className), + 'lib/features/$module/domain/${module}_interactor.dart': + _tplInteractor(module, className), + // Presentation — page (always the same) + 'lib/features/$module/presentation/pages/${module}_page.dart': + _tplPage(module, className), + }; + + // Presentation — state management varies + files.addAll(_stateFiles(module, className, state)); + + for (final e in files.entries) { + File(e.key).writeAsStringSync(e.value); + print(' + ${e.key}'); + } +} + +Map _stateFiles(String module, String className, String state) { + switch (state) { + case 'cubit': + return { + 'lib/features/$module/presentation/blocs/${module}_state.dart': + _tplState(className), + 'lib/features/$module/presentation/blocs/${module}_cubit.dart': + _tplCubit(module, className), + }; + case 'riverpod': + return { + 'lib/features/$module/presentation/providers/${module}_state.dart': + _tplState(className), + 'lib/features/$module/presentation/providers/${module}_notifier.dart': + _tplRiverpodNotifier(module, className), + }; + default: // bloc + return { + 'lib/features/$module/presentation/blocs/${module}_event.dart': + _tplEvent(className), + 'lib/features/$module/presentation/blocs/${module}_state.dart': + _tplState(className), + 'lib/features/$module/presentation/blocs/${module}_bloc.dart': + _tplBloc(module, className), + }; + } +} + +// ─── Storage impl management ────────────────────────────────────────────────── + +String _getActiveProviderName() { + const path = 'lib/core/storage/storage_module.dart'; + if (!File(path).existsSync()) return 'flutter_secure_storage'; + final content = File(path).readAsStringSync(); + final match = RegExp(r'// Active provider: (\S+)').firstMatch(content); + return match?.group(1)?.trim() ?? 'flutter_secure_storage'; +} + +String _implFileName(String provider) => switch (provider) { + 'flutter_secure_storage' => 'secure_storage_provider.dart', + 'sqflite' => 'sqflite_storage_provider.dart', + 'hive' => 'hive_storage_provider.dart', + 'shared_preferences' => 'shared_prefs_storage_provider.dart', + _ => _die('Unknown provider: $provider'), +}; + +String _implFilePath(String p) => 'lib/core/storage/impl/${_implFileName(p)}'; + +void _deleteStorageImpl(String provider) { + final path = _implFilePath(provider); + if (File(path).existsSync()) { + File(path).deleteSync(); + print(' - $path (removed)'); + } +} + +void _generateStorageImpl(String provider) { + final path = _implFilePath(provider); + File(path).writeAsStringSync(_storageImplContent(provider)); + print(' + $path (generated)'); +} + +String _storageImplContent(String provider) => switch (provider) { + 'flutter_secure_storage' => _tplSecureStorageProvider(), + 'sqflite' => _tplSqfliteProvider(), + 'hive' => _tplHiveProvider(), + 'shared_preferences' => _tplSharedPrefsProvider(), + _ => _die('Unknown provider: $provider'), +}; + +void _rewriteStorageModule(String provider) { + final imports = switch (provider) { + 'flutter_secure_storage' => + "import 'package:flutter_secure_storage/flutter_secure_storage.dart';\nimport 'impl/secure_storage_provider.dart';", + 'sqflite' => "import 'impl/sqflite_storage_provider.dart';", + 'hive' => "import 'impl/hive_storage_provider.dart';", + 'shared_preferences' => "import 'impl/shared_prefs_storage_provider.dart';", + _ => _die('Unknown provider: $provider'), + }; + final providerExpr = switch (provider) { + 'flutter_secure_storage' => 'const SecureStorageProvider(FlutterSecureStorage())', + 'sqflite' => 'SqfliteStorageProvider()', + 'hive' => 'HiveStorageProvider()', + 'shared_preferences' => 'SharedPrefsStorageProvider()', + _ => _die('Unknown provider: $provider'), + }; + + const path = 'lib/core/storage/storage_module.dart'; + File(path).writeAsStringSync('''// ============================================================ +// SPL MANAGED FILE — DO NOT EDIT MANUALLY +// Active provider: $provider +// To switch: dart run codegen/spl_manager.dart storage set +// Available: flutter_secure_storage | sqflite | hive | shared_preferences +// ============================================================ + +$imports + +import 'package:injectable/injectable.dart'; +import 'app_storage.dart'; + +@module +abstract class StorageModule { + @lazySingleton + AppStorage get appStorage => $providerExpr; +} +'''); + print(' ~ lib/core/storage/storage_module.dart (updated)'); +} + +// ─── Code templates — State Management ─────────────────────────────────────── + +String _tplState(String className) => ''' +import 'package:equatable/equatable.dart'; + +abstract class ${className}State extends Equatable { + const ${className}State(); + @override + List get props => []; +} + +class ${className}InitialState extends ${className}State { + const ${className}InitialState(); +} + +class ${className}LoadingState extends ${className}State { + const ${className}LoadingState(); +} + +class ${className}SuccessState extends ${className}State { + final dynamic data; + const ${className}SuccessState({required this.data}); + @override + List get props => [data]; +} + +class ${className}ErrorState extends ${className}State { + final String message; + const ${className}ErrorState({required this.message}); + @override + List get props => [message]; +} +'''; + +// ── BLoC ────────────────────────────────────────────────────────────────────── + +String _tplEvent(String className) => ''' +import 'package:equatable/equatable.dart'; + +abstract class ${className}Event extends Equatable { + const ${className}Event(); + @override + List get props => []; +} + +class Get${className}Event extends ${className}Event { + const Get${className}Event(); +} +'''; + +String _tplBloc(String module, String className) => ''' +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; + +import '../../domain/use_cases/${module}_use_cases.dart'; +import '${module}_event.dart'; +import '${module}_state.dart'; + +@Injectable() +class ${className}Bloc extends Bloc<${className}Event, ${className}State> { + final ${className}UseCases _useCases; + + ${className}Bloc(this._useCases) : super(const ${className}InitialState()) { + on(_onGet); + } + + Future _onGet( + Get${className}Event event, + Emitter<${className}State> emit, + ) async { + emit(const ${className}LoadingState()); + final result = await _useCases.getSomething(); + result.fold( + (failure) => emit(${className}ErrorState(message: failure.message ?? '')), + (data) => emit(${className}SuccessState(data: data)), + ); + } +} +'''; + +// ── Cubit ───────────────────────────────────────────────────────────────────── + +String _tplCubit(String module, String className) => ''' +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; + +import '../../domain/use_cases/${module}_use_cases.dart'; +import '${module}_state.dart'; + +// Cubit: no event classes needed. Call methods directly from UI. +// Uses flutter_bloc — same package as Bloc, no extra dependency. +@Injectable() +class ${className}Cubit extends Cubit<${className}State> { + final ${className}UseCases _useCases; + + ${className}Cubit(this._useCases) : super(const ${className}InitialState()); + + Future getSomething() async { + emit(const ${className}LoadingState()); + final result = await _useCases.getSomething(); + result.fold( + (failure) => emit(${className}ErrorState(message: failure.message ?? '')), + (data) => emit(${className}SuccessState(data: data)), + ); + } +} +'''; + +// ── Riverpod ────────────────────────────────────────────────────────────────── + +String _tplRiverpodNotifier(String module, String className) => ''' +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../services/di.dart'; +import '../../domain/use_cases/${module}_use_cases.dart'; +import '${module}_state.dart'; + +// Bridges injectable get_it DI → Riverpod. +// The domain/data layers stay injectable; only the presentation uses Riverpod. +final ${module}UseCasesProvider = Provider<${className}UseCases>( + (ref) => di<${className}UseCases>(), +); + +final ${module}NotifierProvider = + AsyncNotifierProvider.autoDispose<${className}Notifier, ${className}State>( + ${className}Notifier.new, +); + +class ${className}Notifier + extends AutoDisposeAsyncNotifier<${className}State> { + late ${className}UseCases _useCases; + + @override + Future<${className}State> build() async { + _useCases = ref.read(${module}UseCasesProvider); + return const ${className}InitialState(); + } + + Future getSomething() async { + state = const AsyncValue.loading(); + final result = await _useCases.getSomething(); + result.fold( + (failure) => state = + AsyncError(failure.message ?? 'Error', StackTrace.current), + (data) => state = AsyncData(${className}SuccessState(data: data)), + ); + } +} +'''; + +// ─── Storage provider templates ─────────────────────────────────────────────── + +String _tplSecureStorageProvider() => r''' +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import '../app_storage.dart'; + +/// [AppStorage] backed by FlutterSecureStorage. +/// Managed by spl_manager. To switch: dart run codegen/spl_manager.dart storage set +class SecureStorageProvider implements AppStorage { + final FlutterSecureStorage _storage; + const SecureStorageProvider(this._storage); + + @override Future init() async {} + + @override + Future put(String key, dynamic value) async => + _storage.write(key: key, value: value.toString()); + + @override + Future get(String key) async { + final value = await _storage.read(key: key); + if (value == null) return null; + if (T == int) return int.tryParse(value) as T?; + if (T == double) return double.tryParse(value) as T?; + if (T == bool) return (value == 'true') as T?; + return value as T?; + } + + @override Future delete(String key) async => _storage.delete(key: key); + @override Future clear() async => _storage.deleteAll(); + @override Future contains(String key) async => + _storage.containsKey(key: key); +} +'''; + +String _tplSqfliteProvider() => r''' +import 'package:sqflite/sqflite.dart'; +import '../app_storage.dart'; + +/// [AppStorage] backed by sqflite. +/// Call di().init() in main() before runApp(). +class SqfliteStorageProvider implements AppStorage { + Database? _db; + static const _table = 'kv_store'; + + @override + Future init() async { + final path = await getDatabasesPath(); + _db = await openDatabase( + '$path/app_storage.db', + version: 1, + onCreate: (db, _) async => db.execute( + 'CREATE TABLE $_table (key TEXT PRIMARY KEY, value TEXT NOT NULL)', + ), + ); + } + + @override + Future put(String key, dynamic value) async => + _db!.insert(_table, {'key': key, 'value': value.toString()}, + conflictAlgorithm: ConflictAlgorithm.replace); + + @override + Future get(String key) async { + final rows = await _db!.query(_table, where: 'key = ?', whereArgs: [key]); + if (rows.isEmpty) return null; + final raw = rows.first['value'] as String; + if (T == int) return int.tryParse(raw) as T?; + if (T == double) return double.tryParse(raw) as T?; + if (T == bool) return (raw == 'true') as T?; + return raw as T?; + } + + @override Future delete(String key) async => + _db!.delete(_table, where: 'key = ?', whereArgs: [key]); + @override Future clear() async => _db!.delete(_table); + @override Future contains(String key) async { + final rows = await _db!.query(_table, where: 'key = ?', whereArgs: [key]); + return rows.isNotEmpty; + } +} +'''; + +String _tplHiveProvider() => r''' +import 'package:hive_flutter/hive_flutter.dart'; +import '../app_storage.dart'; + +/// [AppStorage] backed by Hive. +/// Requires: hive_flutter: ^1.1.0 in pubspec.yaml +/// Call di().init() in main() before runApp(). +class HiveStorageProvider implements AppStorage { + late Box _box; + + @override + Future init() async { + await Hive.initFlutter(); + _box = await Hive.openBox('app_storage'); + } + + @override Future put(String key, dynamic value) async => _box.put(key, value); + @override Future get(String key) async => _box.get(key) as T?; + @override Future delete(String key) async => _box.delete(key); + @override Future clear() async => _box.clear(); + @override Future contains(String key) async => _box.containsKey(key); +} +'''; + +String _tplSharedPrefsProvider() => r''' +import 'package:shared_preferences/shared_preferences.dart'; +import '../app_storage.dart'; + +/// [AppStorage] backed by SharedPreferences. +/// Requires: shared_preferences: ^2.3.0 in pubspec.yaml +/// Call di().init() in main() before runApp(). +class SharedPrefsStorageProvider implements AppStorage { + late SharedPreferences _prefs; + + @override + Future init() async => _prefs = await SharedPreferences.getInstance(); + + @override + Future put(String key, dynamic value) async { + if (value is int) await _prefs.setInt(key, value); + else if (value is double) await _prefs.setDouble(key, value); + else if (value is bool) await _prefs.setBool(key, value); + else await _prefs.setString(key, value.toString()); + } + + @override Future get(String key) async => _prefs.get(key) as T?; + @override Future delete(String key) async => _prefs.remove(key); + @override Future clear() async => _prefs.clear(); + @override Future contains(String key) async => _prefs.containsKey(key); +} +'''; + +// ─── Data/Domain templates (shared across all state mgmt choices) ───────────── + +String _tplLocalDataSources(String module, String className, + {bool withStorage = false}) { + if (!withStorage) { + return '''import 'package:injectable/injectable.dart'; + +abstract class ${className}LocalDataSources {} + +@LazySingleton(as: ${className}LocalDataSources) +class ${className}LocalDataSourcesImpl implements ${className}LocalDataSources { + const ${className}LocalDataSourcesImpl(); +} +'''; + } + return '''import 'package:boilerplate/core/storage/app_storage.dart'; +import 'package:injectable/injectable.dart'; + +abstract class ${className}LocalDataSources { + Future cache(String key, dynamic value); + Future getCached(String key); + Future clearCache(); +} + +@LazySingleton(as: ${className}LocalDataSources) +class ${className}LocalDataSourcesImpl implements ${className}LocalDataSources { + final AppStorage _storage; + const ${className}LocalDataSourcesImpl(this._storage); + + @override + Future cache(String key, dynamic value) => _storage.put(key, value); + + @override + Future getCached(String key) => _storage.get(key); + + @override + Future clearCache() => _storage.clear(); +} +'''; +} + +String _tplMapper(String module, String className) => ''' +import '../responses/${module}_response.dart'; +import '../../../domain/model/$module.dart'; + +class ${className}Mapper { + static $className mapResponseToDomain(${className}Response response) { + return $className(id: response.id); + } +} +'''; + +String _tplResponse(String module, String className) => ''' +import 'package:freezed_annotation/freezed_annotation.dart'; + +part '${module}_response.freezed.dart'; +part '${module}_response.g.dart'; + +@freezed +abstract class ${className}Response with _\$${className}Response { + const factory ${className}Response({ + required int id, + }) = _${className}Response; + + factory ${className}Response.fromJson(Map json) => + _\$${className}ResponseFromJson(json); +} +'''; + +String _tplRemoteDataSources(String module, String className) => ''' +import 'package:boilerplate/core/client/network_service.dart'; +import 'package:injectable/injectable.dart'; + +import '../model/responses/${module}_response.dart'; + +abstract class ${className}RemoteDataSources { + Future<${className}Response> getSomething(); +} + +@LazySingleton(as: ${className}RemoteDataSources) +class ${className}RemoteDataSourceImpl implements ${className}RemoteDataSources { + final NetworkService _networkService; + const ${className}RemoteDataSourceImpl(this._networkService); + + @override + Future<${className}Response> getSomething() async { + // TODO: implement via _networkService + throw UnimplementedError(); + } +} +'''; + +String _tplRepositoryImpl(String module, String className) => ''' +import 'package:boilerplate/core/client/api_call.dart'; +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +import 'local/${module}_local_data_sources.dart'; +import 'model/mapper/${module}_mapper.dart'; +import 'remote/${module}_remote_data_sources.dart'; +import '../domain/model/$module.dart'; +import '../domain/repository/${module}_repository.dart'; + +@LazySingleton(as: ${className}Repository) +class ${className}RepositoryImpl implements ${className}Repository { + final ${className}RemoteDataSources _remote; + final ${className}LocalDataSources _local; + + const ${className}RepositoryImpl(this._remote, this._local); + + @override + Future> getSomething() { + return apiCall<$className>( + func: _remote.getSomething(), + mapper: (value) => ${className}Mapper.mapResponseToDomain(value), + ); + } +} +'''; + +String _tplModel(String className) => ''' +class $className { + final int id; + const $className({required this.id}); +} +'''; + +String _tplRepository(String module, String className) => ''' +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:dartz/dartz.dart'; + +import '../model/$module.dart'; + +abstract class ${className}Repository { + Future> getSomething(); +} +'''; + +String _tplUseCases(String module, String className) => ''' +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:dartz/dartz.dart'; + +import '../model/$module.dart'; + +abstract class ${className}UseCases { + Future> getSomething(); +} +'''; + +String _tplInteractor(String module, String className) => ''' +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +import 'model/$module.dart'; +import 'repository/${module}_repository.dart'; +import 'use_cases/${module}_use_cases.dart'; + +@LazySingleton(as: ${className}UseCases) +class ${className}Interactor implements ${className}UseCases { + final ${className}Repository _repository; + const ${className}Interactor(this._repository); + + @override + Future> getSomething() => + _repository.getSomething(); +} +'''; + +String _tplPage(String module, String className) => ''' +import 'package:flutter/material.dart'; + +class ${className}Page extends StatelessWidget { + static const route = '/$module'; + const ${className}Page({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('$className')), + body: const Center(child: Text('$className — replace me')), + ); + } +} +'''; + +// ─── spl.yaml helpers ───────────────────────────────────────────────────────── + +Map _readSplConfig() { + const path = 'spl.yaml'; + if (!File(path).existsSync()) _die('spl.yaml not found. Run from project root.'); + + final lines = File(path).readAsLinesSync(); + final config = {}; + String? section; + Map? currentFeature; + + for (final line in lines) { + if (line.trim().startsWith('#') || line.trim().isEmpty) continue; + + if (!line.startsWith(' ') && !line.startsWith('\t')) { + section = line.trim().replaceAll(':', ''); + if (section == 'features') config['features'] = >[]; + continue; + } + + final trimmed = line.trim(); + + if (section == 'app' || section == 'storage' || section == 'state_management') { + final idx = trimmed.indexOf(':'); + if (idx > 0) { + config.putIfAbsent(section!, () => {}); + (config[section] as Map)[trimmed.substring(0, idx).trim()] = + trimmed.substring(idx + 1).trim(); + } + } + + if (section == 'features') { + if (trimmed.startsWith('- name:')) { + currentFeature = {'name': trimmed.replaceFirst('- name:', '').trim()}; + (config['features'] as List).add(currentFeature); + } else if (currentFeature != null) { + final idx = trimmed.indexOf(':'); + if (idx > 0) { + currentFeature[trimmed.substring(0, idx).trim()] = + trimmed.substring(idx + 1).trim(); + } + } + } + } + + return config; +} + +void _addFeatureToConfig(String name, + {required String storage, required String state}) { + const path = 'spl.yaml'; + final content = File(path).readAsStringSync(); + File(path).writeAsStringSync( + '$content\n - name: $name\n status: active\n storage: $storage\n state: $state\n', + ); +} + +void _removeFeatureFromConfig(String name) { + const path = 'spl.yaml'; + final lines = File(path).readAsLinesSync(); + final result = []; + bool skip = false; + + for (final line in lines) { + if (line.trim() == '- name: $name') { + skip = true; + if (result.isNotEmpty && result.last.trim().isEmpty) result.removeLast(); + continue; + } + if (skip) { + if (line.trim().startsWith('- name:') || !line.startsWith(' ')) { + skip = false; + } else { + continue; + } + } + result.add(line); + } + File(path).writeAsStringSync(result.join('\n')); +} + +void _updateFeatureStatusInConfig(String name, String status) { + const path = 'spl.yaml'; + final lines = File(path).readAsLinesSync(); + final result = []; + bool inFeature = false; + bool patched = false; + + for (final line in lines) { + if (line.trim() == '- name: $name') { + inFeature = true; + patched = false; + } else if (inFeature && line.trim().startsWith('status:') && !patched) { + result.add(line.replaceFirst(RegExp(r'status:\s*\w+'), 'status: $status')); + patched = true; + continue; + } else if (inFeature && (line.trim().startsWith('- name:') || !line.startsWith(' '))) { + inFeature = false; + } + result.add(line); + } + File(path).writeAsStringSync(result.join('\n')); +} + +void _updateStorageInConfig(String provider) { + const path = 'spl.yaml'; + File(path).writeAsStringSync( + File(path).readAsStringSync().replaceFirst( + RegExp(r'local_backend:.*'), + 'local_backend: $provider', + ), + ); +} + +void _updateStateDefaultInConfig(String solution) { + const path = 'spl.yaml'; + File(path).writeAsStringSync( + File(path).readAsStringSync().replaceFirst( + RegExp(r'default: (bloc|cubit|riverpod)'), + 'default: $solution', + ), + ); +} + +// ─── Mason integration ──────────────────────────────────────────────────────── + +bool? _masonAvailable; + +Future _checkMason() async { + if (_masonAvailable != null) return _masonAvailable!; + final r = await Process.run('mason', ['--version'], runInShell: true); + _masonAvailable = r.exitCode == 0 && File('.mason/bricks.json').existsSync(); + return _masonAvailable!; +} + +Future _tryMasonFeature(String module, + {bool withStorage = false, String state = 'bloc'}) async { + if (!await _checkMason()) return false; + print(' Using Mason brick: feature'); + final r = await Process.run( + 'mason', + ['make', 'feature', + '--name', module, + '--with_storage', withStorage.toString(), + '--state', state, + '-o', '.', '--no-confirm'], + runInShell: true, + ); + if (r.exitCode != 0) { + print(' Mason failed → falling back to inline templates.'); + return false; + } + print(r.stdout); + return true; +} + +Future _tryMasonStorage(String provider) async { + if (!await _checkMason()) return false; + final brick = switch (provider) { + 'flutter_secure_storage' => 'storage_secure', + 'sqflite' => 'storage_sqflite', + 'hive' => 'storage_hive', + 'shared_preferences' => 'storage_prefs', + _ => null, + }; + if (brick == null) return false; + print(' Using Mason brick: $brick'); + final r = await Process.run( + 'mason', ['make', brick, '-o', '.', '--no-confirm'], + runInShell: true, + ); + if (r.exitCode != 0) { + print(' Mason failed → falling back to inline templates.'); + return false; + } + print(r.stdout); + return true; +} + +// ─── Validation + Notes ─────────────────────────────────────────────────────── + +void _validateStateChoice(String state) { + const valid = ['bloc', 'cubit', 'riverpod']; + if (!valid.contains(state)) { + _die('Unknown state: "$state"\nValid: ${valid.join(' | ')}'); + } +} + +void _printStateNotes(String state) { + if (state == 'riverpod') { + print(''); + print(' ⚠ Riverpod requires: flutter_riverpod in pubspec.yaml'); + print(' ⚠ Add ProviderScope at the root of your widget tree in main()'); + } +} + +void _printStorageNotes(String provider) { + switch (provider) { + case 'hive': + print('\n ⚠ Add: hive_flutter: ^1.1.0 to pubspec.yaml'); + print(' ⚠ Call di().init() in main() before runApp()'); + case 'shared_preferences': + print('\n ⚠ Add: shared_preferences: ^2.3.0 to pubspec.yaml'); + print(' ⚠ Call di().init() in main() before runApp()'); + case 'sqflite': + print('\n ⚠ Call di().init() in main() before runApp()'); + default: + break; + } +} + +// ─── build_runner ───────────────────────────────────────────────────────────── + +Future _runBuildRunner() async { + final result = await Process.run( + 'dart', + ['run', 'build_runner', 'build', '--delete-conflicting-outputs'], + runInShell: true, + ); + if (result.exitCode != 0) { + print('\n${result.stderr}'); + _die('build_runner failed (exit ${result.exitCode})'); + } + print(' build_runner: OK'); +} + +// ─── Utilities ──────────────────────────────────────────────────────────────── + +String _toPascalCase(String s) => s + .split(RegExp(r'[_\s-]+')) + .map((w) => w.isEmpty ? '' : '${w[0].toUpperCase()}${w.substring(1)}') + .join(); + +void _printHeader(String t) { + print(''); + print(' ══ $t ══'); + print(''); +} + +void _printHelp() { + print(''' +SPL Manager — Software Product Line CLI + +Variability: + Storage [XOR] one backend for the whole app + State Mgmt [OR] global default + per-feature override + +Commands: + list + add Scaffold a new feature (active) + add --with-storage Include local cache (AppStorage) + add --state bloc|cubit|riverpod Override state mgmt for this feature + disable Move to catalog — code kept, DI removed + enable Restore from catalog — DI re-wired + remove [--yes|-y] Hard delete (active or catalog) + storage set Switch storage (XOR) + storage list + state set Change default state mgmt + state list + fix Re-run build_runner +'''); +} + +Never _die(String msg) { + stderr.writeln('\n ✗ $msg\n'); + exit(1); +} diff --git a/lib/core/client/network_exception.dart b/lib/core/client/network_exception.dart index fd8268e..a4cbd04 100644 --- a/lib/core/client/network_exception.dart +++ b/lib/core/client/network_exception.dart @@ -96,47 +96,37 @@ class InternalServerErrorException extends NetworkException { } class ConflictException extends NetworkException { - ConflictException({String? message, Response? response}) + ConflictException({super.message, super.response}) : super( - message: message, prefix: 'Conflict', - response: response, ); } class RequestEntityTooLargeException extends NetworkException { - RequestEntityTooLargeException({String? message, Response? response}) + RequestEntityTooLargeException({super.message, super.response}) : super( - message: message, prefix: 'Request Entity Too Large', - response: response, ); } class FetchDataException extends NetworkException { - FetchDataException({String? message, Response? response}) + FetchDataException({super.message, super.response}) : super( - message: message, prefix: 'Error During Communication', - response: response, ); } class NotFoundException extends NetworkException { - NotFoundException({String? message, Response? response}) + NotFoundException({super.message, super.response}) : super( - message: message, prefix: 'Not Found', - response: response, ); } class UnprocessableEntityException extends NetworkException { - UnprocessableEntityException({String? message, Response? response}) + UnprocessableEntityException({super.message, super.response}) : super( - message: message, prefix: 'Invalid Request', - response: response, ); String? getErrorMessage() { @@ -147,11 +137,9 @@ class UnprocessableEntityException extends NetworkException { } class BadRequestException extends NetworkException { - BadRequestException({String? message, Response? response}) + BadRequestException({super.message, super.response}) : super( - message: message, prefix: 'Invalid Request', - response: response, ); String? getErrorMessage() { diff --git a/lib/core/database/secure_database.dart b/lib/core/database/secure_database.dart index 9937c00..7dd05c6 100644 --- a/lib/core/database/secure_database.dart +++ b/lib/core/database/secure_database.dart @@ -1,38 +1,36 @@ -import 'package:boilerplate/services/secure_storage.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:injectable/injectable.dart'; +/// Abstraction for encrypted key-value storage. +/// Always backed by FlutterSecureStorage — this is NOT a variability point. +/// Use this for sensitive data only (tokens, auth keys). +/// +/// For general feature local caching, inject [AppStorage] instead. abstract class SecureDatabase { - Future write({ - required String key, - required String value, - }); - + Future write({required String key, required String value}); Future delete(String key); - Future getString(String key); } @LazySingleton(as: SecureDatabase) class SecureDatabaseImpl implements SecureDatabase { - final SecureStorage _storage; + // FlutterSecureStorage is const — no need to inject it. + static const _storage = FlutterSecureStorage(); - const SecureDatabaseImpl(this._storage); + const SecureDatabaseImpl(); @override Future delete(String key) async { - await _storage.delete(key); + await _storage.delete(key: key); } @override - Future write({ - required String key, - required String value, - }) async { + Future write({required String key, required String value}) async { await _storage.write(key: key, value: value); } @override Future getString(String key) async { - return await _storage.read(key); + return await _storage.read(key: key); } } diff --git a/lib/core/storage/app_storage.dart b/lib/core/storage/app_storage.dart new file mode 100644 index 0000000..720d730 --- /dev/null +++ b/lib/core/storage/app_storage.dart @@ -0,0 +1,25 @@ +/// Abstract interface for general-purpose local storage. +/// +/// This is the SPL variability point for storage backends. +/// Use `dart run codegen/spl_manager.dart storage set ` to switch. +/// Available: flutter_secure_storage | hive | sqflite | shared_preferences +abstract class AppStorage { + /// Must be called once at app startup (e.g., in main()). + Future init(); + + /// Store a value. Supported types depend on the active provider. + /// All providers support: String, int, double, bool, List\. + Future put(String key, dynamic value); + + /// Read a value. Returns null if the key doesn't exist. + Future get(String key); + + /// Delete a single key. + Future delete(String key); + + /// Delete all stored data. + Future clear(); + + /// Returns true if the key exists. + Future contains(String key); +} diff --git a/lib/core/storage/impl/secure_storage_provider.dart b/lib/core/storage/impl/secure_storage_provider.dart new file mode 100644 index 0000000..2fe6d24 --- /dev/null +++ b/lib/core/storage/impl/secure_storage_provider.dart @@ -0,0 +1,30 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import '../app_storage.dart'; + +/// [AppStorage] backed by FlutterSecureStorage. +/// Managed by spl_manager. To switch: dart run codegen/spl_manager.dart storage set +class SecureStorageProvider implements AppStorage { + final FlutterSecureStorage _storage; + const SecureStorageProvider(this._storage); + + @override Future init() async {} + + @override + Future put(String key, dynamic value) async => + _storage.write(key: key, value: value.toString()); + + @override + Future get(String key) async { + final value = await _storage.read(key: key); + if (value == null) return null; + if (T == int) return int.tryParse(value) as T?; + if (T == double) return double.tryParse(value) as T?; + if (T == bool) return (value == 'true') as T?; + return value as T?; + } + + @override Future delete(String key) async => _storage.delete(key: key); + @override Future clear() async => _storage.deleteAll(); + @override Future contains(String key) async => + _storage.containsKey(key: key); +} diff --git a/lib/core/storage/storage_module.dart b/lib/core/storage/storage_module.dart new file mode 100644 index 0000000..a2bda2b --- /dev/null +++ b/lib/core/storage/storage_module.dart @@ -0,0 +1,18 @@ +// ============================================================ +// SPL MANAGED FILE — DO NOT EDIT MANUALLY +// Active provider: flutter_secure_storage +// To switch: dart run codegen/spl_manager.dart storage set +// Available: flutter_secure_storage | sqflite | hive | shared_preferences +// ============================================================ + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'impl/secure_storage_provider.dart'; + +import 'package:injectable/injectable.dart'; +import 'app_storage.dart'; + +@module +abstract class StorageModule { + @lazySingleton + AppStorage get appStorage => const SecureStorageProvider(FlutterSecureStorage()); +} diff --git a/lib/design/widgets/atom/app_text_field.dart b/lib/design/widgets/atom/app_text_field.dart index e8ec811..1d31cb8 100644 --- a/lib/design/widgets/atom/app_text_field.dart +++ b/lib/design/widgets/atom/app_text_field.dart @@ -14,7 +14,7 @@ class AppTextField extends StatefulWidget { final List? inputFormatters; const AppTextField({ - Key? key, + super.key, required this.controller, required this.hint, this.label, @@ -22,7 +22,7 @@ class AppTextField extends StatefulWidget { this.obscureText = false, this.isError = false, this.inputFormatters, - }) : super(key: key); + }); @override State createState() => _AppTextFieldState(); diff --git a/lib/design/widgets/atom/primary_button.dart b/lib/design/widgets/atom/primary_button.dart index afcb7c4..c58478a 100644 --- a/lib/design/widgets/atom/primary_button.dart +++ b/lib/design/widgets/atom/primary_button.dart @@ -9,11 +9,11 @@ class PrimaryButton extends StatelessWidget { final bool isLoading; const PrimaryButton({ - Key? key, + super.key, required this.text, required this.onTap, this.isLoading = false, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/features/authentication/data/remote/model/responses/auth_response.dart b/lib/features/authentication/data/remote/model/responses/auth_response.dart index 0293177..1eaaa74 100644 --- a/lib/features/authentication/data/remote/model/responses/auth_response.dart +++ b/lib/features/authentication/data/remote/model/responses/auth_response.dart @@ -4,7 +4,7 @@ part 'auth_response.freezed.dart'; part 'auth_response.g.dart'; @freezed -class AuthResponse with _$AuthResponse { +abstract class AuthResponse with _$AuthResponse { const factory AuthResponse({ required int id, required String username, diff --git a/lib/features/main/presentation/pages/main_page.dart b/lib/features/main/presentation/pages/main_page.dart index 78a1b76..8013430 100644 --- a/lib/features/main/presentation/pages/main_page.dart +++ b/lib/features/main/presentation/pages/main_page.dart @@ -10,7 +10,7 @@ class MainPage extends StatelessWidget { static const route = '/main'; - const MainPage({Key? key, required this.body}) : super(key: key); + const MainPage({super.key, required this.body}); static const List routes = [ ProductHomePage.route, diff --git a/lib/features/onboarding/data/remote/model/responses/onboarding_response.dart b/lib/features/onboarding/data/remote/model/responses/onboarding_response.dart index 851bb52..eceb3e2 100644 --- a/lib/features/onboarding/data/remote/model/responses/onboarding_response.dart +++ b/lib/features/onboarding/data/remote/model/responses/onboarding_response.dart @@ -4,7 +4,7 @@ part 'onboarding_response.freezed.dart'; part 'onboarding_response.g.dart'; @freezed -class OnboardingResponse with _$OnboardingResponse { +abstract class OnboardingResponse with _$OnboardingResponse { const factory OnboardingResponse({ required int id, required String username, diff --git a/lib/features/product/data/remote/model/responses/list_product_response.dart b/lib/features/product/data/remote/model/responses/list_product_response.dart index 2593a54..2e9a743 100644 --- a/lib/features/product/data/remote/model/responses/list_product_response.dart +++ b/lib/features/product/data/remote/model/responses/list_product_response.dart @@ -5,7 +5,7 @@ part 'list_product_response.freezed.dart'; part 'list_product_response.g.dart'; @freezed -class ListProductResponse with _$ListProductResponse { +abstract class ListProductResponse with _$ListProductResponse { const factory ListProductResponse({ required List products, required int total, diff --git a/lib/features/product/data/remote/model/responses/product_item_response.dart b/lib/features/product/data/remote/model/responses/product_item_response.dart index b458afe..645e0b7 100644 --- a/lib/features/product/data/remote/model/responses/product_item_response.dart +++ b/lib/features/product/data/remote/model/responses/product_item_response.dart @@ -4,7 +4,7 @@ part 'product_item_response.freezed.dart'; part 'product_item_response.g.dart'; @freezed -class ProductItemResponse with _$ProductItemResponse { +abstract class ProductItemResponse with _$ProductItemResponse { const factory ProductItemResponse({ required int id, required String title, diff --git a/lib/features/product/data/remote/model/responses/product_user_response.dart b/lib/features/product/data/remote/model/responses/product_user_response.dart index aaeab34..38991cb 100644 --- a/lib/features/product/data/remote/model/responses/product_user_response.dart +++ b/lib/features/product/data/remote/model/responses/product_user_response.dart @@ -4,7 +4,7 @@ part 'product_user_response.freezed.dart'; part 'product_user_response.g.dart'; @freezed -class ProductUserResponse with _$ProductUserResponse { +abstract class ProductUserResponse with _$ProductUserResponse { const factory ProductUserResponse({ required int id, required String username, diff --git a/lib/features/product/presentation/home/widgets/product_item_widget.dart b/lib/features/product/presentation/home/widgets/product_item_widget.dart index 62b9acb..6079444 100644 --- a/lib/features/product/presentation/home/widgets/product_item_widget.dart +++ b/lib/features/product/presentation/home/widgets/product_item_widget.dart @@ -1,6 +1,5 @@ import 'package:boilerplate/core/constants/assets.dart'; import 'package:flutter/material.dart'; -import 'package:skeletonizer/skeletonizer.dart'; import '../../../../../design/constants/colors.dart'; import '../../../../../design/constants/text_style.dart'; diff --git a/lib/features/profile/data/remote/model/responses/user_response.dart b/lib/features/profile/data/remote/model/responses/user_response.dart index 1d9d00b..757616c 100644 --- a/lib/features/profile/data/remote/model/responses/user_response.dart +++ b/lib/features/profile/data/remote/model/responses/user_response.dart @@ -4,7 +4,7 @@ part 'user_response.freezed.dart'; part 'user_response.g.dart'; @freezed -class UserResponse with _$UserResponse { +abstract class UserResponse with _$UserResponse { const factory UserResponse({ required int id, required String username, diff --git a/lib/services/di.config.dart b/lib/services/di.config.dart index c997237..9ac0a42 100644 --- a/lib/services/di.config.dart +++ b/lib/services/di.config.dart @@ -1,4 +1,5 @@ // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 // ************************************************************************** // InjectableConfigGenerator @@ -15,6 +16,8 @@ import '../core/client/app_environment.dart' as _i119; import '../core/client/network_service.dart' as _i941; import '../core/client/network_utils.dart' as _i936; import '../core/database/secure_database.dart' as _i124; +import '../core/storage/app_storage.dart' as _i812; +import '../core/storage/storage_module.dart' as _i624; import '../features/authentication/data/auth_repository_impl.dart' as _i493; import '../features/authentication/data/local/auth_local_data_sources.dart' as _i981; @@ -52,7 +55,6 @@ import '../features/profile/data/profile_repository_impl.dart' as _i1030; import '../features/profile/data/remote/profile_remote_data_sources.dart' as _i622; import '../features/profile/domain/repository/profile_repository.dart' as _i928; -import 'secure_storage.dart' as _i897; const String _dev = 'dev'; const String _prod = 'prod'; @@ -68,13 +70,16 @@ extension GetItInjectableX on _i174.GetIt { environment, environmentFilter, ); - gh.factory<_i897.SecureStorage>(() => _i897.SecureStorage()); + final storageModule = _$StorageModule(); + gh.lazySingleton<_i812.AppStorage>(() => storageModule.appStorage); + gh.lazySingleton<_i124.SecureDatabase>( + () => const _i124.SecureDatabaseImpl()); + gh.lazySingleton<_i936.NetworkUtils>( + () => _i936.NetworkUtils(gh<_i124.SecureDatabase>())); gh.factory<_i119.AppEnvironment>( () => _i119.DevEnvironment(), registerFor: {_dev}, ); - gh.lazySingleton<_i124.SecureDatabase>( - () => _i124.SecureDatabaseImpl(gh<_i897.SecureStorage>())); gh.lazySingleton<_i1024.ProfileLocalDataSources>( () => _i1024.ProfileLocalDataSourcesImpl(gh<_i124.SecureDatabase>())); gh.lazySingleton<_i981.AuthLocalDataSources>( @@ -83,8 +88,6 @@ extension GetItInjectableX on _i174.GetIt { () => _i119.ProdEnvironment(), registerFor: {_prod}, ); - gh.lazySingleton<_i936.NetworkUtils>( - () => _i936.NetworkUtils(gh<_i124.SecureDatabase>())); gh.lazySingleton<_i941.NetworkService>(() => _i941.NetworkService( environment: gh<_i119.AppEnvironment>(), networkUtils: gh<_i936.NetworkUtils>(), @@ -93,36 +96,38 @@ extension GetItInjectableX on _i174.GetIt { () => _i622.ProfileRemoteDataSourceImpl(gh<_i941.NetworkService>())); gh.lazySingleton<_i24.AuthRemoteDataSources>( () => _i24.AuthRemoteDataSourceImpl(gh<_i941.NetworkService>())); - gh.lazySingleton<_i174.ProductRemoteDataSources>( - () => _i174.ProductRemoteDataSourceImpl(gh<_i941.NetworkService>())); + gh.lazySingleton<_i928.ProfileRepository>( + () => _i1030.ProfileRepositoryImpl( + gh<_i622.ProfileRemoteDataSources>(), + gh<_i1024.ProfileLocalDataSources>(), + )); gh.lazySingleton<_i888.AuthRepository>(() => _i493.AuthRepositoryImpl( gh<_i24.AuthRemoteDataSources>(), gh<_i981.AuthLocalDataSources>(), )); - gh.lazySingleton<_i128.ProductRepository>(() => - _i162.ProductRepositoryImpl(gh<_i174.ProductRemoteDataSources>())); - gh.lazySingleton<_i60.ProductUseCases>( - () => _i283.ProductInteractor(gh<_i128.ProductRepository>())); + gh.lazySingleton<_i174.ProductRemoteDataSources>( + () => _i174.ProductRemoteDataSourceImpl(gh<_i941.NetworkService>())); gh.lazySingleton<_i438.OnboardingRemoteDataSources>( () => _i438.OnboardingRemoteDataSourceImpl(gh<_i941.NetworkService>())); - gh.lazySingleton<_i928.ProfileRepository>( - () => _i1030.ProfileRepositoryImpl( - gh<_i622.ProfileRemoteDataSources>(), - gh<_i1024.ProfileLocalDataSources>(), - )); + gh.lazySingleton<_i128.ProductRepository>(() => + _i162.ProductRepositoryImpl(gh<_i174.ProductRemoteDataSources>())); + gh.lazySingleton<_i521.AuthenticationUseCases>( + () => _i56.AuthenticationInteractor(gh<_i888.AuthRepository>())); gh.lazySingleton<_i998.OnboardingRepository>(() => _i255.OnboardingRepositoryImpl( gh<_i438.OnboardingRemoteDataSources>())); - gh.factory<_i513.ProductHomeBloc>( - () => _i513.ProductHomeBloc(gh<_i60.ProductUseCases>())); gh.lazySingleton<_i1022.OnboardingUseCases>( () => _i698.OnboardingInteractor(gh<_i998.OnboardingRepository>())); - gh.lazySingleton<_i521.AuthenticationUseCases>( - () => _i56.AuthenticationInteractor(gh<_i888.AuthRepository>())); + gh.lazySingleton<_i60.ProductUseCases>( + () => _i283.ProductInteractor(gh<_i128.ProductRepository>())); gh.factory<_i960.AuthenticationBloc>( () => _i960.AuthenticationBloc(gh<_i521.AuthenticationUseCases>())); gh.factory<_i221.OnboardingBloc>( () => _i221.OnboardingBloc(gh<_i1022.OnboardingUseCases>())); + gh.factory<_i513.ProductHomeBloc>( + () => _i513.ProductHomeBloc(gh<_i60.ProductUseCases>())); return this; } } + +class _$StorageModule extends _i624.StorageModule {} diff --git a/lib/services/di.dart b/lib/services/di.dart index 0e25f0c..9b1996a 100644 --- a/lib/services/di.dart +++ b/lib/services/di.dart @@ -1,6 +1,3 @@ -import 'package:boilerplate/core/client/network_service.dart'; -import 'package:boilerplate/core/client/network_utils.dart'; -import 'package:boilerplate/core/constants/secrets.dart'; import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; diff --git a/lib/services/secure_storage.dart b/lib/services/secure_storage.dart deleted file mode 100644 index e7d5524..0000000 --- a/lib/services/secure_storage.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:injectable/injectable.dart'; - -@Injectable() -class SecureStorage { - final storage = const FlutterSecureStorage(); - - Future read(String key) async { - return await storage.read(key: key); - } - - Future write({ - required String key, - required String value, - }) async { - await storage.write(key: key, value: value); - } - - Future delete(String key) async { - await storage.delete(key: key); - } -} diff --git a/mason.yaml b/mason.yaml new file mode 100644 index 0000000..15f4d78 --- /dev/null +++ b/mason.yaml @@ -0,0 +1,14 @@ +bricks: + # Feature scaffold brick — used by spl_manager add + feature: + path: bricks/feature + + # Storage provider bricks — used by spl_manager storage set + storage_secure: + path: bricks/storage_secure + storage_sqflite: + path: bricks/storage_sqflite + storage_hive: + path: bricks/storage_hive + storage_prefs: + path: bricks/storage_prefs diff --git a/pubspec.lock b/pubspec.lock index 994a67c..a94769d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,111 +5,98 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "93.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b + url: "https://pub.dev" + source: hosted + version: "10.0.1" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "2.0.3" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" bloc: dependency: transitive description: name: bloc - sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + sha256: a48653a82055a900b88cd35f92429f068c5a8057ae9b136d197b3d56c57efb81 url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "9.2.0" bloc_test: dependency: "direct main" description: name: bloc_test - sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + sha256: "1dd549e58be35148bc22a9135962106aa29334bc1e3f285994946a1057b29d7b" url: "https://pub.dev" source: hosted - version: "9.1.7" + version: "10.0.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "4.0.4" build_config: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.3.0" build_daemon: dependency: transitive description: name: build_daemon - sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" - url: "https://pub.dev" - source: hosted - version: "4.0.3" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "4.1.1" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" - url: "https://pub.dev" - source: hosted - version: "2.4.14" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: "7981eb922842c77033026eb4341d5af651562008cdb116bdfa31fc46516b6462" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "2.12.2" built_collection: dependency: transitive description: @@ -122,50 +109,66 @@ packages: dependency: transitive description: name: built_value - sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" + sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9" url: "https://pub.dev" source: hosted - version: "8.9.3" + version: "8.12.4" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" code_builder: dependency: transitive description: name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" url: "https://pub.dev" source: hosted - version: "4.10.1" + version: "4.11.1" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" convert: dependency: transitive description: @@ -178,18 +181,18 @@ packages: dependency: transitive description: name: coverage - sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.15.0" crypto: dependency: transitive description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -202,10 +205,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "3.1.7" dartz: dependency: "direct main" description: @@ -226,58 +229,58 @@ packages: dependency: "direct main" description: name: dio - sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.9.2" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.2" envied: dependency: "direct main" description: name: envied - sha256: "129a0dbf32b90344fa2e9d6943569fdec8f17904e66161e0a1f09ee3416508ae" + sha256: "2ca9842c7f513ab527e4f35a58331d6f9a7f90f270b5ba501d73ff2d9fa449ff" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.3.3" envied_generator: dependency: "direct dev" description: name: envied_generator - sha256: "76aec98907872ce8488f021e68d213bd0d9bf224eb393a094be1708cc3180d41" + sha256: "4ed57d61dccb8e546a811b6a3dc5ebcef24bdfe60febbbd6fef31342ab15f9e5" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.3.3" equatable: dependency: "direct main" description: name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.8" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0" file: dependency: transitive description: @@ -303,66 +306,66 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 url: "https://pub.dev" source: hosted - version: "8.1.6" + version: "9.1.1" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage - sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40 url: "https://pub.dev" source: hosted - version: "9.2.4" - flutter_secure_storage_linux: + version: "10.0.0" + flutter_secure_storage_darwin: dependency: transitive description: - name: flutter_secure_storage_linux - sha256: bf7404619d7ab5c0a1151d7c4e802edad8f33535abfbeff2f9e1fe1274e2d705 + name: flutter_secure_storage_darwin + sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3" url: "https://pub.dev" source: hosted - version: "1.2.2" - flutter_secure_storage_macos: + version: "0.2.0" + flutter_secure_storage_linux: dependency: transitive description: - name: flutter_secure_storage_macos - sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + name: flutter_secure_storage_linux + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.0.0" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "2.0.1" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "2.1.0" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "4.1.0" flutter_staggered_grid_view: dependency: transitive description: @@ -375,10 +378,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.2.4" flutter_test: dependency: "direct dev" description: flutter @@ -393,18 +396,18 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: f23ea33b3863f119b58ed1b586e881a46bd28715ddcc4dbc33104524e3434131 url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "3.2.5" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.1.0" frontend_server_client: dependency: transitive description: @@ -417,34 +420,34 @@ packages: dependency: "direct main" description: name: get_it - sha256: f126a3e286b7f5b578bf436d5592968706c4c1de28a228b870ce375d9f743103 + sha256: "568d62f0e68666fb5d95519743b3c24a34c7f19d834b0658c46e26d778461f66" url: "https://pub.dev" source: hosted - version: "8.0.3" + version: "9.2.1" glob: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" go_router: dependency: "direct main" description: name: go_router - sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d" + sha256: "7974313e217a7771557add6ff2238acb63f635317c35fa590d348fb238f00896" url: "https://pub.dev" source: hosted - version: "14.6.3" + version: "17.1.0" google_fonts: dependency: "direct main" description: name: google_fonts - sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + sha256: db9df7a5898d894eeda4c78143f35c30a243558be439518972366880b80bf88e url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "8.0.2" graphs: dependency: transitive description: @@ -453,14 +456,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b + url: "https://pub.dev" + source: hosted + version: "4.3.0" http: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.6.0" http_multi_server: dependency: transitive description: @@ -481,26 +500,26 @@ packages: dependency: "direct main" description: name: infinite_scroll_pagination - sha256: "4047eb8191e8b33573690922a9e995af64c3949dc87efc844f936b039ea279df" + sha256: b0d28e37cd8f62490ff6aef63f9db93d4c78b7f11b7c6b26f33c69d8476fda78 url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "5.1.1" injectable: dependency: "direct main" description: name: injectable - sha256: "5e1556ea1d374fe44cbe846414d9bab346285d3d8a1da5877c01ad0774006068" + sha256: "32b36a9d87f18662bee0b1951b81f47a01f2bf28cd6ea94f60bc5453c7bf598c" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.1+4" injectable_generator: dependency: "direct dev" description: name: injectable_generator - sha256: af403d76c7b18b4217335e0075e950cd0579fd7f8d7bd47ee7c85ada31680ba1 + sha256: fcc0d1edcef2c863dec846b0dec27cb2390d9e9b62e61da5cfc2b9a06b9533c2 url: "https://pub.dev" source: hosted - version: "2.6.2" + version: "2.12.1" io: dependency: transitive description: @@ -509,70 +528,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" json_annotation: dependency: "direct main" description: name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.11.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c + sha256: "44729f5c45748e6748f6b9a57ab8f7e4336edc8ae41fc295070e3814e616a6c0" url: "https://pub.dev" source: hosted - version: "6.9.0" + version: "6.13.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lean_builder: + dependency: transitive + description: + name: lean_builder + sha256: ee4117b03e93a4eb83e1a78c8e7a1dc22188d43bb142309982be48673a1b3a53 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "0.1.7" lints: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.1.0" logger: dependency: "direct main" description: name: logger - sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.2" logging: dependency: transitive description: @@ -581,38 +600,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.0" mime: dependency: transitive description: @@ -629,6 +640,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f" + url: "https://pub.dev" + source: hosted + version: "0.17.5" nested: dependency: transitive description: @@ -645,22 +664,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" package_config: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_parsing: dependency: transitive description: @@ -681,18 +708,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.22" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -721,10 +748,10 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "7.0.2" platform: dependency: transitive description: @@ -745,26 +772,26 @@ packages: dependency: transitive description: name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.2" provider: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5+1" pub_semver: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pubspec_parse: dependency: transitive description: @@ -809,18 +836,18 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" skeletonizer: dependency: "direct main" description: name: skeletonizer - sha256: "0dcacc51c144af4edaf37672072156f49e47036becbc394d7c51850c5c1e884b" + sha256: "9f38f9b47ec3cf2235a6a4f154a88a95432bc55ba98b3e2eb6ced5c1974bc122" url: "https://pub.dev" source: hosted - version: "1.4.3" + version: "2.1.3" sky_engine: dependency: transitive description: flutter @@ -838,18 +865,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: adc962c96fffb2de1728ef396a995aaedcafbe635abdca13d2a987ce17e57751 url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "4.2.1" source_helper: dependency: transitive description: name: source_helper - sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + sha256: "4a85e90b50694e652075cbe4575665539d253e6ec10e46e76b45368ab5e3caae" url: "https://pub.dev" source: hosted - version: "1.3.5" + version: "1.3.10" source_map_stack_trace: dependency: transitive description: @@ -870,42 +897,42 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.2" sqflite: dependency: "direct main" description: name: sqflite - sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" sqflite_android: dependency: transitive description: name: sqflite_android - sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2+3" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" url: "https://pub.dev" source: hosted - version: "2.5.4+6" + version: "2.5.6" sqflite_darwin: dependency: transitive description: name: sqflite_darwin - sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" url: "https://pub.dev" source: hosted - version: "2.4.1+1" + version: "2.4.2" sqflite_platform_interface: dependency: transitive description: @@ -918,18 +945,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: @@ -942,58 +969,50 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" synchronized: dependency: transitive description: name: synchronized - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 url: "https://pub.dev" source: hosted - version: "3.3.0+3" + version: "3.4.0" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test: dependency: transitive description: name: test - sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" url: "https://pub.dev" source: hosted - version: "1.25.8" + version: "1.29.0" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.9" test_core: dependency: transitive description: name: test_core - sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" url: "https://pub.dev" source: hosted - version: "0.6.5" - timing: - dependency: transitive - description: - name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" - url: "https://pub.dev" - source: hosted - version: "1.0.2" + version: "0.6.15" typed_data: dependency: transitive description: @@ -1006,10 +1025,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" + sha256: "7076216a10d5c390315fbe536a30f1254c341e7543e6c4c8a815e591307772b1" url: "https://pub.dev" source: hosted - version: "1.1.15" + version: "1.1.20" vector_graphics_codec: dependency: transitive description: @@ -1022,58 +1041,58 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" url: "https://pub.dev" source: hosted - version: "1.1.16" + version: "1.2.0" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "15.0.2" watcher: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.1" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" webkit_inspection_protocol: dependency: transitive description: @@ -1086,10 +1105,10 @@ packages: dependency: transitive description: name: win32 - sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e url: "https://pub.dev" source: hosted - version: "5.10.0" + version: "5.15.0" xdg_directories: dependency: transitive description: @@ -1102,10 +1121,18 @@ packages: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + xxh3: + dependency: transitive + description: + name: xxh3 + sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "1.2.0" yaml: dependency: transitive description: @@ -1115,5 +1142,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index 5153ca5..21d5f81 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # B - bloc_test: ^9.1.3 + bloc_test: ^10.0.0 # C cupertino_icons: ^1.0.2 # D @@ -44,16 +44,16 @@ dependencies: envied: ^1.0.0 equatable: ^2.0.5 # F - freezed_annotation: ^2.4.1 - flutter_bloc: ^8.1.3 - flutter_secure_storage: ^9.2.4 + freezed_annotation: ^3.1.0 + flutter_bloc: ^9.1.1 + flutter_secure_storage: ^10.0.0 flutter_svg: ^2.0.10+1 # G - go_router: ^14.6.3 - get_it: ^8.0.3 - google_fonts: ^6.2.1 + go_router: ^17.1.0 + get_it: ^9.2.1 + google_fonts: ^8.0.2 # I - infinite_scroll_pagination: ^4.1.0 + infinite_scroll_pagination: ^5.1.1 injectable: ^2.5.0 # J json_annotation: ^4.8.1 @@ -62,7 +62,7 @@ dependencies: # M mocktail: ^1.0.3 # S - skeletonizer: ^1.1.0 + skeletonizer: ^2.1.3 sqflite: ^2.4.1 dev_dependencies: @@ -70,7 +70,7 @@ dev_dependencies: sdk: flutter build_runner: ^2.4.14 envied_generator: ^1.0.0 - freezed: ^2.4.1 + freezed: ^3.2.5 injectable_generator: ^2.6.2 json_serializable: ^6.9.0 @@ -79,7 +79,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/spl.yaml b/spl.yaml new file mode 100644 index 0000000..4030c48 --- /dev/null +++ b/spl.yaml @@ -0,0 +1,71 @@ +# ============================================================ +# Software Product Line Configuration +# ============================================================ +# Manage this file via CLI: +# dart run codegen/spl_manager.dart +# +# Commands: +# list Show all features + active variants +# add [opts] Scaffold a new feature +# --with-storage Include AppStorage-backed local cache +# --state bloc|cubit|riverpod Override state management for this feature +# remove [--yes|-y] Delete a feature and rebuild DI +# storage set Switch storage backend (exclusive/XOR) +# storage list Show available storage providers +# state set Change global state management default +# state list Show state management options +# fix Run build_runner +# ============================================================ + +app: + name: boilerplate + package: boilerplate + +# ── Storage variability (XOR — exactly one active) ─────────────────────────── +storage: + # Options: flutter_secure_storage | sqflite | hive | shared_preferences + # Only the active provider has an impl file in lib/core/storage/impl/ + local_backend: flutter_secure_storage + + # Always flutter_secure_storage — NOT a variability point + secure_backend: flutter_secure_storage + +# ── State management variability (OR — global default + per-feature override) ─ +state_management: + # Default for new features. Each feature can override with --state flag. + # Options: bloc | cubit | riverpod + # bloc — flutter_bloc (Events + States + Bloc). Explicit, verbose, traceable. + # cubit — flutter_bloc (States + Cubit). Simpler, fewer files, same package. + # riverpod — flutter_riverpod (Notifier + Provider). Different DI model. + default: bloc + + # Note on exclusivity: + # Storage is XOR: one backend for the whole app. + # State management is NOT XOR: different features can use different solutions. + # Bloc and Cubit coexist freely (same package). Riverpod can coexist too + # but requires flutter_riverpod in pubspec.yaml. + +features: + - name: authentication + status: active + storage: flutter_secure_storage + state: bloc + description: Login/logout and token management (template example) + + - name: onboarding + status: active + storage: none + state: bloc + description: Splash and onboarding flow (template example) + + - name: product + status: active + storage: none + state: bloc + description: Product listing and search (template example) + + - name: profile + status: active + storage: flutter_secure_storage + state: bloc + description: User profile management (template example) \ No newline at end of file From a8e70af1047d78d57a48a2fe606502ff4fafb771 Mon Sep 17 00:00:00 2001 From: MHibriziF Date: Sun, 15 Mar 2026 22:46:27 +0700 Subject: [PATCH 05/10] docs: change deprecated command flutter pub run to dart run --- README.md | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 72c8bd9..b116cf4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # 👨‍💻 Flutter Boilerplate -[![Generic badge](https://img.shields.io/badge/Flutter-v3.27.1-blue)](https://flutter.dev/docs) -[![Generic badge](https://img.shields.io/badge/Dart-v3.6.0-blue)](https://dart.dev/guides) +[![Generic badge](https://img.shields.io/badge/Flutter-v3.41.3-blue)](https://flutter.dev/docs)[![Generic badge](https://img.shields.io/badge/Dart-v3.11.1-blue)](https://dart.dev/guides) Flutter Template @@ -16,7 +15,7 @@ Example how to run development app ``` flutter clean flutter pub get -flutter pub run build_runner build --delete-conflicting-outputs +dart run build_runner build --delete-conflicting-outputs flutter run ``` @@ -25,14 +24,14 @@ Example how to run production app ``` flutter clean flutter pub get -flutter pub run build_runner build --delete-conflicting-outputs +dart run build_runner build --delete-conflicting-outputs flutter build apk -t lib/main_production.dart ``` ### ⚙️ Supported Flavor -1. staging -2. production +1. staging +2. production ### 🎯 Architecture & Pattern @@ -80,8 +79,7 @@ mason get The SPL CLI (`spl_manager.dart`) uses Mason automatically when available. You can also invoke bricks directly: ``` -mason make feature --name \ -mason make feature --name \ --with_storage true --state cubit +mason make feature --name mason make feature --name --with_storage true --state cubit ``` Bricks are excluded from Dart analysis (`analysis_options.yaml`) because they contain Mustache syntax (`{{name.pascalCase()}}`), not valid Dart. @@ -100,8 +98,7 @@ snake_case for file and folder. ### :capital_abcd: Git flow -Commit rules: -(feat|fix|docs|style|refactor|perf|test|build|ci):\/\* +Commit rules:(feat|fix|docs|style|refactor|perf|test|build|ci):/\* feat: A new feature @@ -131,11 +128,10 @@ ci: refactor analysis job #### before push -1. flutter analyze -2. flutter test +1. flutter analyze +2. flutter test -branch rules: -(feature|hotfix|coldfix|service|integration|ui)\/\/\* +branch rules:(feature|hotfix|coldfix|service|integration|ui)//\* ### How to contribute From 0dfe9b22077cb80f19f4a2a7e4348a4c78f05783 Mon Sep 17 00:00:00 2001 From: MHibriziF Date: Sun, 15 Mar 2026 23:04:55 +0700 Subject: [PATCH 06/10] fix: add missing injection --- .vscode/settings.json | 3 +++ android/gradle/wrapper/gradle-wrapper.properties | 3 ++- android/settings.gradle | 4 ++-- lib/features/profile/domain/profile_interactor.dart | 2 ++ .../profile/presentation/blocs/authentication_bloc.dart | 2 ++ lib/services/di.config.dart | 8 ++++++++ 6 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c5f3f6b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 7bb2df6..f89f765 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip +networkTimeout=300000 diff --git a/android/settings.gradle b/android/settings.gradle index a42444d..4f52071 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.2.1" apply false - id "org.jetbrains.kotlin.android" version "1.8.22" apply false + id "com.android.application" version "8.6.0" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/lib/features/profile/domain/profile_interactor.dart b/lib/features/profile/domain/profile_interactor.dart index 7726627..89b3d6d 100644 --- a/lib/features/profile/domain/profile_interactor.dart +++ b/lib/features/profile/domain/profile_interactor.dart @@ -2,9 +2,11 @@ import 'package:boilerplate/core/client/network_exception.dart'; import 'package:boilerplate/features/profile/domain/repository/profile_repository.dart'; import 'package:boilerplate/features/profile/domain/use_cases/profile_use_cases.dart'; import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; import 'model/user.dart'; +@LazySingleton(as: ProfileUseCases) class ProfileInteractor implements ProfileUseCases { final ProfileRepository _repository; diff --git a/lib/features/profile/presentation/blocs/authentication_bloc.dart b/lib/features/profile/presentation/blocs/authentication_bloc.dart index 8bc21f1..94f931c 100644 --- a/lib/features/profile/presentation/blocs/authentication_bloc.dart +++ b/lib/features/profile/presentation/blocs/authentication_bloc.dart @@ -1,5 +1,6 @@ import 'package:boilerplate/features/profile/presentation/blocs/states/get_user_states.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; import '../../domain/use_cases/profile_use_cases.dart'; import 'events/get_user_event.dart'; @@ -7,6 +8,7 @@ import 'events/log_out_event.dart'; import 'profile_events.dart'; import 'profile_states.dart'; +@Injectable() class ProfileBloc extends Bloc { final ProfileUseCases _useCases; diff --git a/lib/services/di.config.dart b/lib/services/di.config.dart index 9ac0a42..3f25419 100644 --- a/lib/services/di.config.dart +++ b/lib/services/di.config.dart @@ -54,7 +54,11 @@ import '../features/profile/data/local/profile_local_data_sources.dart' import '../features/profile/data/profile_repository_impl.dart' as _i1030; import '../features/profile/data/remote/profile_remote_data_sources.dart' as _i622; +import '../features/profile/domain/profile_interactor.dart' as _i40; import '../features/profile/domain/repository/profile_repository.dart' as _i928; +import '../features/profile/domain/use_cases/profile_use_cases.dart' as _i483; +import '../features/profile/presentation/blocs/authentication_bloc.dart' + as _i957; const String _dev = 'dev'; const String _prod = 'prod'; @@ -109,8 +113,12 @@ extension GetItInjectableX on _i174.GetIt { () => _i174.ProductRemoteDataSourceImpl(gh<_i941.NetworkService>())); gh.lazySingleton<_i438.OnboardingRemoteDataSources>( () => _i438.OnboardingRemoteDataSourceImpl(gh<_i941.NetworkService>())); + gh.lazySingleton<_i483.ProfileUseCases>( + () => _i40.ProfileInteractor(gh<_i928.ProfileRepository>())); gh.lazySingleton<_i128.ProductRepository>(() => _i162.ProductRepositoryImpl(gh<_i174.ProductRemoteDataSources>())); + gh.factory<_i957.ProfileBloc>( + () => _i957.ProfileBloc(gh<_i483.ProfileUseCases>())); gh.lazySingleton<_i521.AuthenticationUseCases>( () => _i56.AuthenticationInteractor(gh<_i888.AuthRepository>())); gh.lazySingleton<_i998.OnboardingRepository>(() => From 0f075fb5e8f8c5ca0ac25c07ac98003be515a2c3 Mon Sep 17 00:00:00 2001 From: MHibriziF Date: Sun, 15 Mar 2026 23:38:28 +0700 Subject: [PATCH 07/10] feat: add feature with test, add route --- codegen/spl_manager.dart | 350 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 344 insertions(+), 6 deletions(-) diff --git a/codegen/spl_manager.dart b/codegen/spl_manager.dart index a033564..92d64c0 100644 --- a/codegen/spl_manager.dart +++ b/codegen/spl_manager.dart @@ -10,7 +10,7 @@ /// /// Commands: /// list -/// add [--with-storage] [--state bloc|cubit|riverpod] +/// add [--with-storage] [--with-test] [--shell-route] [--state bloc|cubit|riverpod] /// disable Move feature to catalog (keeps code, unwires DI) /// enable Restore feature from catalog (wires DI) /// remove [--yes|-y] Hard delete (works on active or catalog features) @@ -32,13 +32,19 @@ void main(List args) async { case 'list': await _cmdList(); case 'add': - if (args.length < 2) _die('Usage: add [--with-storage] [--state bloc|cubit|riverpod]'); - final withStorage = args.contains('--with-storage'); - final stateIdx = args.indexOf('--state'); + if (args.length < 2) _die('Usage: add [--with-storage] [--with-test] [--shell-route] [--state bloc|cubit|riverpod]'); + final withStorage = args.contains('--with-storage'); + final withTest = args.contains('--with-test'); + final shellRoute = args.contains('--shell-route'); + final stateIdx = args.indexOf('--state'); final stateOverride = stateIdx != -1 && stateIdx + 1 < args.length ? args[stateIdx + 1] : null; - await _cmdAdd(args[1], withStorage: withStorage, stateOverride: stateOverride); + await _cmdAdd(args[1], + withStorage: withStorage, + withTest: withTest, + shellRoute: shellRoute, + stateOverride: stateOverride); case 'disable': if (args.length < 2) _die('Usage: disable '); await _cmdDisable(args[1]); @@ -137,6 +143,8 @@ Future _cmdList() async { Future _cmdAdd( String name, { bool withStorage = false, + bool withTest = false, + bool shellRoute = false, String? stateOverride, }) async { final module = name.toLowerCase().replaceAll(RegExp(r'[^a-z0-9_]'), '_'); @@ -163,6 +171,8 @@ Future _cmdAdd( print(' Class : $className'); print(' Storage : ${withStorage ? globalStorageBackend : 'none'}'); print(' State : $stateChoice${stateOverride != null ? ' (override)' : ' (default)'}'); + print(' Route : ${shellRoute ? 'shell (bottom nav)' : 'top-level'}'); + print(' Tests : ${withTest ? 'yes (--with-test)' : 'no'}'); print(' DI : auto-wired via build_runner (@injectable)'); print(''); @@ -179,13 +189,16 @@ Future _cmdAdd( state: stateChoice, ); + _injectRoute(module, className, shellRoute: shellRoute); + + if (withTest) _generateTestFiles(module, className, state: stateChoice); + _printStateNotes(stateChoice); print('\n Wiring DI (build_runner)...'); await _runBuildRunner(); print('\n ✓ Done! lib/features/$module/'); - print(' → Register the route in lib/core/router/app_router_config.dart'); } Future _cmdDisable(String name) async { @@ -206,6 +219,7 @@ Future _cmdDisable(String name) async { Directory(activeDir).renameSync(catalogDir); print(' ○ Moved: $activeDir → $catalogDir'); + _removeRoute(module); _updateFeatureStatusInConfig(module, 'inactive'); print(' Regenerating DI...'); @@ -263,6 +277,8 @@ Future _cmdRemove(String name, {bool force = false}) async { Directory(location).deleteSync(recursive: true); print(' Deleted: $location'); + _removeRoute(module); + _removeTests(module); _removeFeatureFromConfig(module); if (inActive) { @@ -456,6 +472,135 @@ Map _stateFiles(String module, String className, String state) { } } +// ─── Route injection ────────────────────────────────────────────────────────── + +void _injectRoute(String module, String className, {bool shellRoute = false}) { + const routerPath = 'lib/core/router/app_router_config.dart'; + if (!File(routerPath).existsSync()) { + print(' ⚠ Router not found at $routerPath — skipping route injection.'); + print(' Register the route manually.'); + return; + } + + var content = File(routerPath).readAsStringSync(); + final pageImport = + "import 'package:boilerplate/features/$module/presentation/pages/${module}_page.dart';"; + + if (content.contains('${className}Page.route')) { + print(' ~ Route for $className already exists — skipping.'); + return; + } + + // Add import — insert before 'import package:flutter' + content = content.replaceFirst( + "import 'package:flutter/", + "$pageImport\nimport 'package:flutter/", + ); + + if (shellRoute) { + // Find the last GoRoute inside ShellRoute and append after it + const anchor = "builder: (context, state) => const ProfilePage())"; + final newEntry = "\n GoRoute(\n" + " path: ${className}Page.route,\n" + " name: ${className}Page.route,\n" + " parentNavigatorKey: _shellKey,\n" + " pageBuilder: (context, state) =>\n" + " const NoTransitionPage(child: ${className}Page()),\n" + " builder: (context, state) => const ${className}Page())"; + content = content.replaceFirst(anchor, '$anchor$newEntry'); + } else { + // Insert top-level GoRoute before ShellRoute( + const anchor = ' ShellRoute('; + final newEntry = " GoRoute(\n" + " path: ${className}Page.route,\n" + " name: ${className}Page.route,\n" + " builder: (context, state) => const ${className}Page()),\n"; + content = content.replaceFirst(anchor, '$newEntry ShellRoute('); + } + + File(routerPath).writeAsStringSync(content); + print(' ~ lib/core/router/app_router_config.dart (route injected)'); +} + +void _removeRoute(String module) { + const routerPath = 'lib/core/router/app_router_config.dart'; + if (!File(routerPath).existsSync()) return; + + final className = _toPascalCase(module); + var content = File(routerPath).readAsStringSync(); + final before = content.length; + + // Remove the import line + content = content.replaceAll( + "import 'package:boilerplate/features/$module/presentation/pages/${module}_page.dart';\n", + '', + ); + + // Remove top-level GoRoute (exact format we generate) + content = content.replaceAll( + " GoRoute(\n" + " path: ${className}Page.route,\n" + " name: ${className}Page.route,\n" + " builder: (context, state) => const ${className}Page()),\n", + '', + ); + + // Remove shell GoRoute (exact format we generate) + content = content.replaceAll( + "\n GoRoute(\n" + " path: ${className}Page.route,\n" + " name: ${className}Page.route,\n" + " parentNavigatorKey: _shellKey,\n" + " pageBuilder: (context, state) =>\n" + " const NoTransitionPage(child: ${className}Page()),\n" + " builder: (context, state) => const ${className}Page())", + '', + ); + + if (content.length != before) { + File(routerPath).writeAsStringSync(content); + print(' ~ lib/core/router/app_router_config.dart (route removed)'); + } +} + +void _removeTests(String module) { + final testDir = Directory('test/features/$module'); + if (testDir.existsSync()) { + testDir.deleteSync(recursive: true); + print(' Deleted: test/features/$module'); + } +} + +// ─── Test file generation ───────────────────────────────────────────────────── + +void _generateTestFiles(String module, String className, {String state = 'bloc'}) { + final testDir = 'test/features/$module'; + Directory('$testDir/domain').createSync(recursive: true); + Directory('$testDir/presentation').createSync(recursive: true); + + final files = { + '$testDir/domain/${module}_interactor_test.dart': + _tplInteractorTest(module, className), + }; + + switch (state) { + case 'cubit': + files['$testDir/presentation/${module}_cubit_test.dart'] = + _tplCubitTest(module, className); + case 'riverpod': + files['$testDir/presentation/${module}_notifier_test.dart'] = + _tplRiverpodTest(module, className); + default: // bloc + files['$testDir/presentation/${module}_bloc_test.dart'] = + _tplBlocTest(module, className); + } + + for (final e in files.entries) { + File(e.key).writeAsStringSync(e.value); + print(' + ${e.key}'); + } +} + // ─── Storage impl management ────────────────────────────────────────────────── String _getActiveProviderName() { @@ -1009,6 +1154,197 @@ class ${className}Page extends StatelessWidget { } '''; +// ─── Test templates ─────────────────────────────────────────────────────────── + +String _tplInteractorTest(String module, String className) => ''' +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:boilerplate/features/$module/domain/${module}_interactor.dart'; +import 'package:boilerplate/features/$module/domain/model/$module.dart'; +import 'package:boilerplate/features/$module/domain/repository/${module}_repository.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class Mock${className}Repository extends Mock implements ${className}Repository {} + +void main() { + late ${className}Interactor interactor; + late Mock${className}Repository mockRepository; + + setUp(() { + mockRepository = Mock${className}Repository(); + interactor = ${className}Interactor(mockRepository); + }); + + group('${className}Interactor', () { + test('getSomething returns data on success', () async { + when(() => mockRepository.getSomething()) + .thenAnswer((_) async => Right($className(id: 1))); + + final result = await interactor.getSomething(); + + expect(result.isRight(), true); + verify(() => mockRepository.getSomething()).called(1); + }); + + test('getSomething returns failure on error', () async { + when(() => mockRepository.getSomething()) + .thenAnswer((_) async => Left(NetworkException(message: 'error'))); + + final result = await interactor.getSomething(); + + expect(result.isLeft(), true); + }); + }); +} +'''; + +String _tplBlocTest(String module, String className) => ''' +import 'package:bloc_test/bloc_test.dart'; +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:boilerplate/features/$module/domain/model/$module.dart'; +import 'package:boilerplate/features/$module/domain/use_cases/${module}_use_cases.dart'; +import 'package:boilerplate/features/$module/presentation/blocs/${module}_bloc.dart'; +import 'package:boilerplate/features/$module/presentation/blocs/${module}_event.dart'; +import 'package:boilerplate/features/$module/presentation/blocs/${module}_state.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class Mock${className}UseCases extends Mock implements ${className}UseCases {} + +void main() { + late Mock${className}UseCases mockUseCases; + + setUp(() { + mockUseCases = Mock${className}UseCases(); + }); + + group('${className}Bloc', () { + blocTest<${className}Bloc, ${className}State>( + 'emits [Loading, Success] when getSomething succeeds', + build: () => ${className}Bloc(mockUseCases), + setUp: () { + when(() => mockUseCases.getSomething()) + .thenAnswer((_) async => Right($className(id: 1))); + }, + act: (bloc) => bloc.add(const Get${className}Event()), + expect: () => [ + const ${className}LoadingState(), + isA<${className}SuccessState>(), + ], + ); + + blocTest<${className}Bloc, ${className}State>( + 'emits [Loading, Error] when getSomething fails', + build: () => ${className}Bloc(mockUseCases), + setUp: () { + when(() => mockUseCases.getSomething()) + .thenAnswer((_) async => Left(NetworkException(message: 'error'))); + }, + act: (bloc) => bloc.add(const Get${className}Event()), + expect: () => [ + const ${className}LoadingState(), + isA<${className}ErrorState>(), + ], + ); + }); +} +'''; + +String _tplCubitTest(String module, String className) => ''' +import 'package:bloc_test/bloc_test.dart'; +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:boilerplate/features/$module/domain/model/$module.dart'; +import 'package:boilerplate/features/$module/domain/use_cases/${module}_use_cases.dart'; +import 'package:boilerplate/features/$module/presentation/blocs/${module}_cubit.dart'; +import 'package:boilerplate/features/$module/presentation/blocs/${module}_state.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class Mock${className}UseCases extends Mock implements ${className}UseCases {} + +void main() { + late Mock${className}UseCases mockUseCases; + + setUp(() { + mockUseCases = Mock${className}UseCases(); + }); + + group('${className}Cubit', () { + blocTest<${className}Cubit, ${className}State>( + 'emits [Loading, Success] when getSomething succeeds', + build: () => ${className}Cubit(mockUseCases), + setUp: () { + when(() => mockUseCases.getSomething()) + .thenAnswer((_) async => Right($className(id: 1))); + }, + act: (cubit) => cubit.getSomething(), + expect: () => [ + const ${className}LoadingState(), + isA<${className}SuccessState>(), + ], + ); + + blocTest<${className}Cubit, ${className}State>( + 'emits [Loading, Error] when getSomething fails', + build: () => ${className}Cubit(mockUseCases), + setUp: () { + when(() => mockUseCases.getSomething()) + .thenAnswer((_) async => Left(NetworkException(message: 'error'))); + }, + act: (cubit) => cubit.getSomething(), + expect: () => [ + const ${className}LoadingState(), + isA<${className}ErrorState>(), + ], + ); + }); +} +'''; + +String _tplRiverpodTest(String module, String className) => ''' +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:boilerplate/features/$module/domain/model/$module.dart'; +import 'package:boilerplate/features/$module/domain/use_cases/${module}_use_cases.dart'; +import 'package:boilerplate/features/$module/presentation/providers/${module}_notifier.dart'; +import 'package:boilerplate/features/$module/presentation/providers/${module}_state.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class Mock${className}UseCases extends Mock implements ${className}UseCases {} + +void main() { + late Mock${className}UseCases mockUseCases; + + setUp(() { + mockUseCases = Mock${className}UseCases(); + }); + + ProviderContainer makeContainer() => ProviderContainer( + overrides: [ + ${module}UseCasesProvider.overrideWithValue(mockUseCases), + ], + ); + + group('${className}Notifier', () { + test('initial state is ${className}InitialState', () async { + when(() => mockUseCases.getSomething()) + .thenAnswer((_) async => Right($className(id: 1))); + + final container = makeContainer(); + addTearDown(container.dispose); + + final state = await container.read(${module}NotifierProvider.future); + expect(state, isA<${className}InitialState>()); + }); + }); +} +'''; + // ─── spl.yaml helpers ───────────────────────────────────────────────────────── Map _readSplConfig() { @@ -1260,6 +1596,8 @@ Commands: list add Scaffold a new feature (active) add --with-storage Include local cache (AppStorage) + add --with-test Generate unit + state mgmt tests + add --shell-route Register as shell (bottom nav) route add --state bloc|cubit|riverpod Override state mgmt for this feature disable Move to catalog — code kept, DI removed enable Restore from catalog — DI re-wired From ff3634b3d67cc6eb29153c0508d2177a291110be Mon Sep 17 00:00:00 2001 From: MHibriziF Date: Sun, 15 Mar 2026 23:46:09 +0700 Subject: [PATCH 08/10] refactor: slice spl_manager to multiple files --- codegen/spl_manager.dart | 1540 +----------------------------- codegen/src/commands.dart | 305 ++++++ codegen/src/generators.dart | 218 +++++ codegen/src/spl_config.dart | 181 ++++ codegen/src/storage_manager.dart | 83 ++ codegen/src/templates.dart | 665 +++++++++++++ codegen/src/utils.dart | 93 ++ 7 files changed, 1552 insertions(+), 1533 deletions(-) create mode 100644 codegen/src/commands.dart create mode 100644 codegen/src/generators.dart create mode 100644 codegen/src/spl_config.dart create mode 100644 codegen/src/storage_manager.dart create mode 100644 codegen/src/templates.dart create mode 100644 codegen/src/utils.dart diff --git a/codegen/spl_manager.dart b/codegen/spl_manager.dart index 92d64c0..2786dac 100644 --- a/codegen/spl_manager.dart +++ b/codegen/spl_manager.dart @@ -23,6 +23,13 @@ library; import 'dart:io'; +part 'src/commands.dart'; +part 'src/generators.dart'; +part 'src/storage_manager.dart'; +part 'src/templates.dart'; +part 'src/spl_config.dart'; +part 'src/utils.dart'; + // ─── Entry point ────────────────────────────────────────────────────────────── void main(List args) async { @@ -81,1536 +88,3 @@ void main(List args) async { _die('Unknown command: ${args[0]}'); } } - -// ─── Commands ───────────────────────────────────────────────────────────────── - -Future _cmdList() async { - final config = _readSplConfig(); - _printHeader('SPL Configuration'); - - final storage = config['storage']?['local_backend'] ?? 'flutter_secure_storage'; - final stateDefault = config['state_management']?['default'] ?? 'bloc'; - - print(' App : ${config['app']?['name'] ?? 'unknown'}'); - print(' Storage [XOR] : $storage'); - print(' State Mgmt [OR] : $stateDefault (default, per-feature override allowed)'); - print(''); - - final features = config['features'] as List>? ?? []; - if (features.isEmpty) { - print(' No features yet.'); - print(' dart run codegen/spl_manager.dart add '); - return; - } - - final active = features.where((f) => (f['status'] ?? 'active') == 'active').toList(); - final inactive = features.where((f) => (f['status'] ?? 'active') == 'inactive').toList(); - - print(' Active features [compiled + DI-wired]:'); - if (active.isEmpty) { - print(' (none)'); - } else { - for (final f in active) { - final name = f['name'] ?? '?'; - final storage = f['storage'] ?? 'none'; - final state = f['state'] ?? stateDefault; - final desc = f['description'] ?? ''; - final storageTag = storage == 'none' ? '' : ' storage:$storage'; - print(' ✓ $name state:$state$storageTag'); - if (desc.isNotEmpty && desc != '""') print(' $desc'); - } - } - - if (inactive.isNotEmpty) { - print(''); - print(' Catalog [code preserved, not compiled]:'); - for (final f in inactive) { - final name = f['name'] ?? '?'; - final storage = f['storage'] ?? 'none'; - final state = f['state'] ?? stateDefault; - final desc = f['description'] ?? ''; - final storageTag = storage == 'none' ? '' : ' storage:$storage'; - print(' ○ $name state:$state$storageTag'); - if (desc.isNotEmpty && desc != '""') print(' $desc'); - } - } - - print(''); - print(' Tip: dart run codegen/spl_manager.dart storage list'); - print(' dart run codegen/spl_manager.dart state list'); -} - -Future _cmdAdd( - String name, { - bool withStorage = false, - bool withTest = false, - bool shellRoute = false, - String? stateOverride, -}) async { - final module = name.toLowerCase().replaceAll(RegExp(r'[^a-z0-9_]'), '_'); - final className = _toPascalCase(module); - final featureDir = 'lib/features/$module'; - - if (Directory(featureDir).existsSync()) { - _die('Feature "$module" already exists at $featureDir'); - } - if (Directory('features_catalog/$module').existsSync()) { - _die('Feature "$module" exists in the catalog (disabled).\n' - ' To restore it: dart run codegen/spl_manager.dart enable $module\n' - ' To delete it: dart run codegen/spl_manager.dart remove $module'); - } - - final config = _readSplConfig(); - final globalStorageBackend = config['storage']?['local_backend'] ?? 'flutter_secure_storage'; - final globalStateDefault = config['state_management']?['default'] ?? 'bloc'; - final stateChoice = stateOverride ?? globalStateDefault; - - _validateStateChoice(stateChoice); - - _printHeader('Adding feature: $module'); - print(' Class : $className'); - print(' Storage : ${withStorage ? globalStorageBackend : 'none'}'); - print(' State : $stateChoice${stateOverride != null ? ' (override)' : ' (default)'}'); - print(' Route : ${shellRoute ? 'shell (bottom nav)' : 'top-level'}'); - print(' Tests : ${withTest ? 'yes (--with-test)' : 'no'}'); - print(' DI : auto-wired via build_runner (@injectable)'); - print(''); - - final usedMason = await _tryMasonFeature(module, - withStorage: withStorage, state: stateChoice); - if (!usedMason) { - _generateFeatureFiles(module, className, - withStorage: withStorage, state: stateChoice); - } - - _addFeatureToConfig( - module, - storage: withStorage ? globalStorageBackend : 'none', - state: stateChoice, - ); - - _injectRoute(module, className, shellRoute: shellRoute); - - if (withTest) _generateTestFiles(module, className, state: stateChoice); - - _printStateNotes(stateChoice); - - print('\n Wiring DI (build_runner)...'); - await _runBuildRunner(); - - print('\n ✓ Done! lib/features/$module/'); -} - -Future _cmdDisable(String name) async { - final module = name.toLowerCase(); - final activeDir = 'lib/features/$module'; - final catalogDir = 'features_catalog/$module'; - - _printHeader('Disabling feature: $module'); - - if (!Directory(activeDir).existsSync()) { - if (Directory(catalogDir).existsSync()) { - _die('Feature "$module" is already disabled (in catalog).'); - } - _die('Feature "$module" not found.'); - } - - Directory('features_catalog').createSync(); - Directory(activeDir).renameSync(catalogDir); - print(' ○ Moved: $activeDir → $catalogDir'); - - _removeRoute(module); - _updateFeatureStatusInConfig(module, 'inactive'); - - print(' Regenerating DI...'); - await _runBuildRunner(); - print('\n ✓ Feature "$module" disabled.'); - print(' → Restore with: dart run codegen/spl_manager.dart enable $module'); -} - -Future _cmdEnable(String name) async { - final module = name.toLowerCase(); - final activeDir = 'lib/features/$module'; - final catalogDir = 'features_catalog/$module'; - - _printHeader('Enabling feature: $module'); - - if (!Directory(catalogDir).existsSync()) { - if (Directory(activeDir).existsSync()) { - _die('Feature "$module" is already active.'); - } - _die('Feature "$module" not found in catalog.\n' - ' Add it fresh: dart run codegen/spl_manager.dart add $module'); - } - - Directory('lib/features').createSync(recursive: true); - Directory(catalogDir).renameSync(activeDir); - print(' ✓ Moved: $catalogDir → $activeDir'); - - _updateFeatureStatusInConfig(module, 'active'); - - print(' Wiring DI (build_runner)...'); - await _runBuildRunner(); - print('\n ✓ Feature "$module" enabled.'); - print(' → Ensure route is registered in lib/core/router/app_router_config.dart'); -} - -Future _cmdRemove(String name, {bool force = false}) async { - final module = name.toLowerCase(); - final activeDir = 'lib/features/$module'; - final catalogDir = 'features_catalog/$module'; - - final inActive = Directory(activeDir).existsSync(); - final inCatalog = Directory(catalogDir).existsSync(); - - if (!inActive && !inCatalog) _die('Feature "$module" not found.'); - - final location = inActive ? activeDir : catalogDir; - _printHeader('Removing feature: $module'); - print(' Location: $location${inCatalog ? ' (disabled)' : ' (active)'}'); - - if (!force) { - stdout.write(' Permanently delete "$module"? [y/N] '); - final confirm = stdin.readLineSync()?.toLowerCase(); - if (confirm != 'y' && confirm != 'yes') { print(' Aborted.'); exit(0); } - } - - Directory(location).deleteSync(recursive: true); - print(' Deleted: $location'); - _removeRoute(module); - _removeTests(module); - _removeFeatureFromConfig(module); - - if (inActive) { - print(' Regenerating DI...'); - await _runBuildRunner(); - } - print('\n ✓ Feature "$module" permanently removed.'); -} - -Future _cmdStorageSet(String provider) async { - const valid = ['flutter_secure_storage', 'sqflite', 'hive', 'shared_preferences']; - if (!valid.contains(provider)) { - _die('Unknown provider: "$provider"\nValid: ${valid.join(' | ')}'); - } - - final current = _getActiveProviderName(); - if (current == provider) { print('\n Already using "$provider".'); exit(0); } - - _printHeader('Switching storage [XOR]: $current → $provider'); - - _deleteStorageImpl(current); - - final usedMason = await _tryMasonStorage(provider); - if (!usedMason) _generateStorageImpl(provider); - - _rewriteStorageModule(provider); - _updateStorageInConfig(provider); - - print('\n Regenerating DI...'); - await _runBuildRunner(); - - print('\n ✓ Storage → "$provider"'); - _printStorageNotes(provider); -} - -void _cmdStorageList() { - _printHeader('Storage Providers [XOR — exactly one active]'); - final current = _getActiveProviderName(); - final providers = { - 'flutter_secure_storage': 'Encrypted key-value. Strings only. Best for sensitive data.', - 'sqflite': 'SQLite (relational). Best for structured/queryable data.', - 'hive': 'NoSQL box store. Fast reads. Best for object graphs.', - 'shared_preferences': 'Simple key-value. Non-encrypted. Best for user settings.', - }; - for (final e in providers.entries) { - final active = e.key == current ? ' ◀ active' : ''; - print(' ${e.key}$active'); - print(' ${e.value}'); - print(''); - } - print(' Switch (XOR): dart run codegen/spl_manager.dart storage set '); -} - -void _cmdStateSet(String solution) { - _validateStateChoice(solution); - _updateStateDefaultInConfig(solution); - _printHeader('State Management Default → $solution'); - print(' Updated spl.yaml default.'); - print(' Existing features are unchanged.'); - print(' New features will use: $solution'); - _printStateNotes(solution); -} - -void _cmdStateList() { - _printHeader('State Management [OR — global default + per-feature override]'); - final config = _readSplConfig(); - final current = config['state_management']?['default'] ?? 'bloc'; - - final solutions = { - 'bloc': [ - 'flutter_bloc (already in pubspec)', - 'Event + State + Bloc. Explicit event stream. Best for complex flows.', - 'Files: _event.dart _state.dart _bloc.dart', - ], - 'cubit': [ - 'flutter_bloc (already in pubspec, same package as bloc)', - 'State + Cubit only. No event classes. Simpler, fewer files.', - 'Files: _state.dart _cubit.dart', - ], - 'riverpod': [ - 'flutter_riverpod (add to pubspec if not present)', - 'Notifier + Provider. Different DI model. Bridges to get_it via di().', - 'Files: _state.dart _notifier.dart', - ], - }; - - for (final e in solutions.entries) { - final active = e.key == current ? ' ◀ default' : ''; - print(' ${e.key}$active'); - for (final line in e.value) print(' $line'); - print(''); - } - - print(' Change default : dart run codegen/spl_manager.dart state set '); - print(' Per-feature : dart run codegen/spl_manager.dart add --state '); - print(''); - print(' Note: bloc and cubit coexist freely (same package).'); - print(' riverpod requires flutter_riverpod in pubspec.yaml.'); -} - -Future _cmdFix() async { - _printHeader('Running build_runner'); - await _runBuildRunner(); - print(' ✓ Done'); -} - -// ─── Feature file generation ────────────────────────────────────────────────── - -void _generateFeatureFiles( - String module, - String className, { - bool withStorage = false, - String state = 'bloc', -}) { - final dirs = [ - 'lib/features/$module/data/local', - 'lib/features/$module/data/model/mapper', - 'lib/features/$module/data/model/responses', - 'lib/features/$module/data/remote', - 'lib/features/$module/domain/model', - 'lib/features/$module/domain/repository', - 'lib/features/$module/domain/use_cases', - if (state == 'riverpod') - 'lib/features/$module/presentation/providers' - else - 'lib/features/$module/presentation/blocs', - 'lib/features/$module/presentation/pages', - 'lib/features/$module/presentation/widgets', - ]; - for (final d in dirs) Directory(d).createSync(recursive: true); - - final files = { - // Data layer - 'lib/features/$module/data/local/${module}_local_data_sources.dart': - _tplLocalDataSources(module, className, withStorage: withStorage), - 'lib/features/$module/data/model/mapper/${module}_mapper.dart': - _tplMapper(module, className), - 'lib/features/$module/data/model/responses/${module}_response.dart': - _tplResponse(module, className), - 'lib/features/$module/data/remote/${module}_remote_data_sources.dart': - _tplRemoteDataSources(module, className), - 'lib/features/$module/data/${module}_repository_impl.dart': - _tplRepositoryImpl(module, className), - // Domain layer - 'lib/features/$module/domain/model/$module.dart': _tplModel(className), - 'lib/features/$module/domain/repository/${module}_repository.dart': - _tplRepository(module, className), - 'lib/features/$module/domain/use_cases/${module}_use_cases.dart': - _tplUseCases(module, className), - 'lib/features/$module/domain/${module}_interactor.dart': - _tplInteractor(module, className), - // Presentation — page (always the same) - 'lib/features/$module/presentation/pages/${module}_page.dart': - _tplPage(module, className), - }; - - // Presentation — state management varies - files.addAll(_stateFiles(module, className, state)); - - for (final e in files.entries) { - File(e.key).writeAsStringSync(e.value); - print(' + ${e.key}'); - } -} - -Map _stateFiles(String module, String className, String state) { - switch (state) { - case 'cubit': - return { - 'lib/features/$module/presentation/blocs/${module}_state.dart': - _tplState(className), - 'lib/features/$module/presentation/blocs/${module}_cubit.dart': - _tplCubit(module, className), - }; - case 'riverpod': - return { - 'lib/features/$module/presentation/providers/${module}_state.dart': - _tplState(className), - 'lib/features/$module/presentation/providers/${module}_notifier.dart': - _tplRiverpodNotifier(module, className), - }; - default: // bloc - return { - 'lib/features/$module/presentation/blocs/${module}_event.dart': - _tplEvent(className), - 'lib/features/$module/presentation/blocs/${module}_state.dart': - _tplState(className), - 'lib/features/$module/presentation/blocs/${module}_bloc.dart': - _tplBloc(module, className), - }; - } -} - -// ─── Route injection ────────────────────────────────────────────────────────── - -void _injectRoute(String module, String className, {bool shellRoute = false}) { - const routerPath = 'lib/core/router/app_router_config.dart'; - if (!File(routerPath).existsSync()) { - print(' ⚠ Router not found at $routerPath — skipping route injection.'); - print(' Register the route manually.'); - return; - } - - var content = File(routerPath).readAsStringSync(); - final pageImport = - "import 'package:boilerplate/features/$module/presentation/pages/${module}_page.dart';"; - - if (content.contains('${className}Page.route')) { - print(' ~ Route for $className already exists — skipping.'); - return; - } - - // Add import — insert before 'import package:flutter' - content = content.replaceFirst( - "import 'package:flutter/", - "$pageImport\nimport 'package:flutter/", - ); - - if (shellRoute) { - // Find the last GoRoute inside ShellRoute and append after it - const anchor = "builder: (context, state) => const ProfilePage())"; - final newEntry = "\n GoRoute(\n" - " path: ${className}Page.route,\n" - " name: ${className}Page.route,\n" - " parentNavigatorKey: _shellKey,\n" - " pageBuilder: (context, state) =>\n" - " const NoTransitionPage(child: ${className}Page()),\n" - " builder: (context, state) => const ${className}Page())"; - content = content.replaceFirst(anchor, '$anchor$newEntry'); - } else { - // Insert top-level GoRoute before ShellRoute( - const anchor = ' ShellRoute('; - final newEntry = " GoRoute(\n" - " path: ${className}Page.route,\n" - " name: ${className}Page.route,\n" - " builder: (context, state) => const ${className}Page()),\n"; - content = content.replaceFirst(anchor, '$newEntry ShellRoute('); - } - - File(routerPath).writeAsStringSync(content); - print(' ~ lib/core/router/app_router_config.dart (route injected)'); -} - -void _removeRoute(String module) { - const routerPath = 'lib/core/router/app_router_config.dart'; - if (!File(routerPath).existsSync()) return; - - final className = _toPascalCase(module); - var content = File(routerPath).readAsStringSync(); - final before = content.length; - - // Remove the import line - content = content.replaceAll( - "import 'package:boilerplate/features/$module/presentation/pages/${module}_page.dart';\n", - '', - ); - - // Remove top-level GoRoute (exact format we generate) - content = content.replaceAll( - " GoRoute(\n" - " path: ${className}Page.route,\n" - " name: ${className}Page.route,\n" - " builder: (context, state) => const ${className}Page()),\n", - '', - ); - - // Remove shell GoRoute (exact format we generate) - content = content.replaceAll( - "\n GoRoute(\n" - " path: ${className}Page.route,\n" - " name: ${className}Page.route,\n" - " parentNavigatorKey: _shellKey,\n" - " pageBuilder: (context, state) =>\n" - " const NoTransitionPage(child: ${className}Page()),\n" - " builder: (context, state) => const ${className}Page())", - '', - ); - - if (content.length != before) { - File(routerPath).writeAsStringSync(content); - print(' ~ lib/core/router/app_router_config.dart (route removed)'); - } -} - -void _removeTests(String module) { - final testDir = Directory('test/features/$module'); - if (testDir.existsSync()) { - testDir.deleteSync(recursive: true); - print(' Deleted: test/features/$module'); - } -} - -// ─── Test file generation ───────────────────────────────────────────────────── - -void _generateTestFiles(String module, String className, {String state = 'bloc'}) { - final testDir = 'test/features/$module'; - Directory('$testDir/domain').createSync(recursive: true); - Directory('$testDir/presentation').createSync(recursive: true); - - final files = { - '$testDir/domain/${module}_interactor_test.dart': - _tplInteractorTest(module, className), - }; - - switch (state) { - case 'cubit': - files['$testDir/presentation/${module}_cubit_test.dart'] = - _tplCubitTest(module, className); - case 'riverpod': - files['$testDir/presentation/${module}_notifier_test.dart'] = - _tplRiverpodTest(module, className); - default: // bloc - files['$testDir/presentation/${module}_bloc_test.dart'] = - _tplBlocTest(module, className); - } - - for (final e in files.entries) { - File(e.key).writeAsStringSync(e.value); - print(' + ${e.key}'); - } -} - -// ─── Storage impl management ────────────────────────────────────────────────── - -String _getActiveProviderName() { - const path = 'lib/core/storage/storage_module.dart'; - if (!File(path).existsSync()) return 'flutter_secure_storage'; - final content = File(path).readAsStringSync(); - final match = RegExp(r'// Active provider: (\S+)').firstMatch(content); - return match?.group(1)?.trim() ?? 'flutter_secure_storage'; -} - -String _implFileName(String provider) => switch (provider) { - 'flutter_secure_storage' => 'secure_storage_provider.dart', - 'sqflite' => 'sqflite_storage_provider.dart', - 'hive' => 'hive_storage_provider.dart', - 'shared_preferences' => 'shared_prefs_storage_provider.dart', - _ => _die('Unknown provider: $provider'), -}; - -String _implFilePath(String p) => 'lib/core/storage/impl/${_implFileName(p)}'; - -void _deleteStorageImpl(String provider) { - final path = _implFilePath(provider); - if (File(path).existsSync()) { - File(path).deleteSync(); - print(' - $path (removed)'); - } -} - -void _generateStorageImpl(String provider) { - final path = _implFilePath(provider); - File(path).writeAsStringSync(_storageImplContent(provider)); - print(' + $path (generated)'); -} - -String _storageImplContent(String provider) => switch (provider) { - 'flutter_secure_storage' => _tplSecureStorageProvider(), - 'sqflite' => _tplSqfliteProvider(), - 'hive' => _tplHiveProvider(), - 'shared_preferences' => _tplSharedPrefsProvider(), - _ => _die('Unknown provider: $provider'), -}; - -void _rewriteStorageModule(String provider) { - final imports = switch (provider) { - 'flutter_secure_storage' => - "import 'package:flutter_secure_storage/flutter_secure_storage.dart';\nimport 'impl/secure_storage_provider.dart';", - 'sqflite' => "import 'impl/sqflite_storage_provider.dart';", - 'hive' => "import 'impl/hive_storage_provider.dart';", - 'shared_preferences' => "import 'impl/shared_prefs_storage_provider.dart';", - _ => _die('Unknown provider: $provider'), - }; - final providerExpr = switch (provider) { - 'flutter_secure_storage' => 'const SecureStorageProvider(FlutterSecureStorage())', - 'sqflite' => 'SqfliteStorageProvider()', - 'hive' => 'HiveStorageProvider()', - 'shared_preferences' => 'SharedPrefsStorageProvider()', - _ => _die('Unknown provider: $provider'), - }; - - const path = 'lib/core/storage/storage_module.dart'; - File(path).writeAsStringSync('''// ============================================================ -// SPL MANAGED FILE — DO NOT EDIT MANUALLY -// Active provider: $provider -// To switch: dart run codegen/spl_manager.dart storage set -// Available: flutter_secure_storage | sqflite | hive | shared_preferences -// ============================================================ - -$imports - -import 'package:injectable/injectable.dart'; -import 'app_storage.dart'; - -@module -abstract class StorageModule { - @lazySingleton - AppStorage get appStorage => $providerExpr; -} -'''); - print(' ~ lib/core/storage/storage_module.dart (updated)'); -} - -// ─── Code templates — State Management ─────────────────────────────────────── - -String _tplState(String className) => ''' -import 'package:equatable/equatable.dart'; - -abstract class ${className}State extends Equatable { - const ${className}State(); - @override - List get props => []; -} - -class ${className}InitialState extends ${className}State { - const ${className}InitialState(); -} - -class ${className}LoadingState extends ${className}State { - const ${className}LoadingState(); -} - -class ${className}SuccessState extends ${className}State { - final dynamic data; - const ${className}SuccessState({required this.data}); - @override - List get props => [data]; -} - -class ${className}ErrorState extends ${className}State { - final String message; - const ${className}ErrorState({required this.message}); - @override - List get props => [message]; -} -'''; - -// ── BLoC ────────────────────────────────────────────────────────────────────── - -String _tplEvent(String className) => ''' -import 'package:equatable/equatable.dart'; - -abstract class ${className}Event extends Equatable { - const ${className}Event(); - @override - List get props => []; -} - -class Get${className}Event extends ${className}Event { - const Get${className}Event(); -} -'''; - -String _tplBloc(String module, String className) => ''' -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:injectable/injectable.dart'; - -import '../../domain/use_cases/${module}_use_cases.dart'; -import '${module}_event.dart'; -import '${module}_state.dart'; - -@Injectable() -class ${className}Bloc extends Bloc<${className}Event, ${className}State> { - final ${className}UseCases _useCases; - - ${className}Bloc(this._useCases) : super(const ${className}InitialState()) { - on(_onGet); - } - - Future _onGet( - Get${className}Event event, - Emitter<${className}State> emit, - ) async { - emit(const ${className}LoadingState()); - final result = await _useCases.getSomething(); - result.fold( - (failure) => emit(${className}ErrorState(message: failure.message ?? '')), - (data) => emit(${className}SuccessState(data: data)), - ); - } -} -'''; - -// ── Cubit ───────────────────────────────────────────────────────────────────── - -String _tplCubit(String module, String className) => ''' -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:injectable/injectable.dart'; - -import '../../domain/use_cases/${module}_use_cases.dart'; -import '${module}_state.dart'; - -// Cubit: no event classes needed. Call methods directly from UI. -// Uses flutter_bloc — same package as Bloc, no extra dependency. -@Injectable() -class ${className}Cubit extends Cubit<${className}State> { - final ${className}UseCases _useCases; - - ${className}Cubit(this._useCases) : super(const ${className}InitialState()); - - Future getSomething() async { - emit(const ${className}LoadingState()); - final result = await _useCases.getSomething(); - result.fold( - (failure) => emit(${className}ErrorState(message: failure.message ?? '')), - (data) => emit(${className}SuccessState(data: data)), - ); - } -} -'''; - -// ── Riverpod ────────────────────────────────────────────────────────────────── - -String _tplRiverpodNotifier(String module, String className) => ''' -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../../../../services/di.dart'; -import '../../domain/use_cases/${module}_use_cases.dart'; -import '${module}_state.dart'; - -// Bridges injectable get_it DI → Riverpod. -// The domain/data layers stay injectable; only the presentation uses Riverpod. -final ${module}UseCasesProvider = Provider<${className}UseCases>( - (ref) => di<${className}UseCases>(), -); - -final ${module}NotifierProvider = - AsyncNotifierProvider.autoDispose<${className}Notifier, ${className}State>( - ${className}Notifier.new, -); - -class ${className}Notifier - extends AutoDisposeAsyncNotifier<${className}State> { - late ${className}UseCases _useCases; - - @override - Future<${className}State> build() async { - _useCases = ref.read(${module}UseCasesProvider); - return const ${className}InitialState(); - } - - Future getSomething() async { - state = const AsyncValue.loading(); - final result = await _useCases.getSomething(); - result.fold( - (failure) => state = - AsyncError(failure.message ?? 'Error', StackTrace.current), - (data) => state = AsyncData(${className}SuccessState(data: data)), - ); - } -} -'''; - -// ─── Storage provider templates ─────────────────────────────────────────────── - -String _tplSecureStorageProvider() => r''' -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import '../app_storage.dart'; - -/// [AppStorage] backed by FlutterSecureStorage. -/// Managed by spl_manager. To switch: dart run codegen/spl_manager.dart storage set -class SecureStorageProvider implements AppStorage { - final FlutterSecureStorage _storage; - const SecureStorageProvider(this._storage); - - @override Future init() async {} - - @override - Future put(String key, dynamic value) async => - _storage.write(key: key, value: value.toString()); - - @override - Future get(String key) async { - final value = await _storage.read(key: key); - if (value == null) return null; - if (T == int) return int.tryParse(value) as T?; - if (T == double) return double.tryParse(value) as T?; - if (T == bool) return (value == 'true') as T?; - return value as T?; - } - - @override Future delete(String key) async => _storage.delete(key: key); - @override Future clear() async => _storage.deleteAll(); - @override Future contains(String key) async => - _storage.containsKey(key: key); -} -'''; - -String _tplSqfliteProvider() => r''' -import 'package:sqflite/sqflite.dart'; -import '../app_storage.dart'; - -/// [AppStorage] backed by sqflite. -/// Call di().init() in main() before runApp(). -class SqfliteStorageProvider implements AppStorage { - Database? _db; - static const _table = 'kv_store'; - - @override - Future init() async { - final path = await getDatabasesPath(); - _db = await openDatabase( - '$path/app_storage.db', - version: 1, - onCreate: (db, _) async => db.execute( - 'CREATE TABLE $_table (key TEXT PRIMARY KEY, value TEXT NOT NULL)', - ), - ); - } - - @override - Future put(String key, dynamic value) async => - _db!.insert(_table, {'key': key, 'value': value.toString()}, - conflictAlgorithm: ConflictAlgorithm.replace); - - @override - Future get(String key) async { - final rows = await _db!.query(_table, where: 'key = ?', whereArgs: [key]); - if (rows.isEmpty) return null; - final raw = rows.first['value'] as String; - if (T == int) return int.tryParse(raw) as T?; - if (T == double) return double.tryParse(raw) as T?; - if (T == bool) return (raw == 'true') as T?; - return raw as T?; - } - - @override Future delete(String key) async => - _db!.delete(_table, where: 'key = ?', whereArgs: [key]); - @override Future clear() async => _db!.delete(_table); - @override Future contains(String key) async { - final rows = await _db!.query(_table, where: 'key = ?', whereArgs: [key]); - return rows.isNotEmpty; - } -} -'''; - -String _tplHiveProvider() => r''' -import 'package:hive_flutter/hive_flutter.dart'; -import '../app_storage.dart'; - -/// [AppStorage] backed by Hive. -/// Requires: hive_flutter: ^1.1.0 in pubspec.yaml -/// Call di().init() in main() before runApp(). -class HiveStorageProvider implements AppStorage { - late Box _box; - - @override - Future init() async { - await Hive.initFlutter(); - _box = await Hive.openBox('app_storage'); - } - - @override Future put(String key, dynamic value) async => _box.put(key, value); - @override Future get(String key) async => _box.get(key) as T?; - @override Future delete(String key) async => _box.delete(key); - @override Future clear() async => _box.clear(); - @override Future contains(String key) async => _box.containsKey(key); -} -'''; - -String _tplSharedPrefsProvider() => r''' -import 'package:shared_preferences/shared_preferences.dart'; -import '../app_storage.dart'; - -/// [AppStorage] backed by SharedPreferences. -/// Requires: shared_preferences: ^2.3.0 in pubspec.yaml -/// Call di().init() in main() before runApp(). -class SharedPrefsStorageProvider implements AppStorage { - late SharedPreferences _prefs; - - @override - Future init() async => _prefs = await SharedPreferences.getInstance(); - - @override - Future put(String key, dynamic value) async { - if (value is int) await _prefs.setInt(key, value); - else if (value is double) await _prefs.setDouble(key, value); - else if (value is bool) await _prefs.setBool(key, value); - else await _prefs.setString(key, value.toString()); - } - - @override Future get(String key) async => _prefs.get(key) as T?; - @override Future delete(String key) async => _prefs.remove(key); - @override Future clear() async => _prefs.clear(); - @override Future contains(String key) async => _prefs.containsKey(key); -} -'''; - -// ─── Data/Domain templates (shared across all state mgmt choices) ───────────── - -String _tplLocalDataSources(String module, String className, - {bool withStorage = false}) { - if (!withStorage) { - return '''import 'package:injectable/injectable.dart'; - -abstract class ${className}LocalDataSources {} - -@LazySingleton(as: ${className}LocalDataSources) -class ${className}LocalDataSourcesImpl implements ${className}LocalDataSources { - const ${className}LocalDataSourcesImpl(); -} -'''; - } - return '''import 'package:boilerplate/core/storage/app_storage.dart'; -import 'package:injectable/injectable.dart'; - -abstract class ${className}LocalDataSources { - Future cache(String key, dynamic value); - Future getCached(String key); - Future clearCache(); -} - -@LazySingleton(as: ${className}LocalDataSources) -class ${className}LocalDataSourcesImpl implements ${className}LocalDataSources { - final AppStorage _storage; - const ${className}LocalDataSourcesImpl(this._storage); - - @override - Future cache(String key, dynamic value) => _storage.put(key, value); - - @override - Future getCached(String key) => _storage.get(key); - - @override - Future clearCache() => _storage.clear(); -} -'''; -} - -String _tplMapper(String module, String className) => ''' -import '../responses/${module}_response.dart'; -import '../../../domain/model/$module.dart'; - -class ${className}Mapper { - static $className mapResponseToDomain(${className}Response response) { - return $className(id: response.id); - } -} -'''; - -String _tplResponse(String module, String className) => ''' -import 'package:freezed_annotation/freezed_annotation.dart'; - -part '${module}_response.freezed.dart'; -part '${module}_response.g.dart'; - -@freezed -abstract class ${className}Response with _\$${className}Response { - const factory ${className}Response({ - required int id, - }) = _${className}Response; - - factory ${className}Response.fromJson(Map json) => - _\$${className}ResponseFromJson(json); -} -'''; - -String _tplRemoteDataSources(String module, String className) => ''' -import 'package:boilerplate/core/client/network_service.dart'; -import 'package:injectable/injectable.dart'; - -import '../model/responses/${module}_response.dart'; - -abstract class ${className}RemoteDataSources { - Future<${className}Response> getSomething(); -} - -@LazySingleton(as: ${className}RemoteDataSources) -class ${className}RemoteDataSourceImpl implements ${className}RemoteDataSources { - final NetworkService _networkService; - const ${className}RemoteDataSourceImpl(this._networkService); - - @override - Future<${className}Response> getSomething() async { - // TODO: implement via _networkService - throw UnimplementedError(); - } -} -'''; - -String _tplRepositoryImpl(String module, String className) => ''' -import 'package:boilerplate/core/client/api_call.dart'; -import 'package:boilerplate/core/client/network_exception.dart'; -import 'package:dartz/dartz.dart'; -import 'package:injectable/injectable.dart'; - -import 'local/${module}_local_data_sources.dart'; -import 'model/mapper/${module}_mapper.dart'; -import 'remote/${module}_remote_data_sources.dart'; -import '../domain/model/$module.dart'; -import '../domain/repository/${module}_repository.dart'; - -@LazySingleton(as: ${className}Repository) -class ${className}RepositoryImpl implements ${className}Repository { - final ${className}RemoteDataSources _remote; - final ${className}LocalDataSources _local; - - const ${className}RepositoryImpl(this._remote, this._local); - - @override - Future> getSomething() { - return apiCall<$className>( - func: _remote.getSomething(), - mapper: (value) => ${className}Mapper.mapResponseToDomain(value), - ); - } -} -'''; - -String _tplModel(String className) => ''' -class $className { - final int id; - const $className({required this.id}); -} -'''; - -String _tplRepository(String module, String className) => ''' -import 'package:boilerplate/core/client/network_exception.dart'; -import 'package:dartz/dartz.dart'; - -import '../model/$module.dart'; - -abstract class ${className}Repository { - Future> getSomething(); -} -'''; - -String _tplUseCases(String module, String className) => ''' -import 'package:boilerplate/core/client/network_exception.dart'; -import 'package:dartz/dartz.dart'; - -import '../model/$module.dart'; - -abstract class ${className}UseCases { - Future> getSomething(); -} -'''; - -String _tplInteractor(String module, String className) => ''' -import 'package:boilerplate/core/client/network_exception.dart'; -import 'package:dartz/dartz.dart'; -import 'package:injectable/injectable.dart'; - -import 'model/$module.dart'; -import 'repository/${module}_repository.dart'; -import 'use_cases/${module}_use_cases.dart'; - -@LazySingleton(as: ${className}UseCases) -class ${className}Interactor implements ${className}UseCases { - final ${className}Repository _repository; - const ${className}Interactor(this._repository); - - @override - Future> getSomething() => - _repository.getSomething(); -} -'''; - -String _tplPage(String module, String className) => ''' -import 'package:flutter/material.dart'; - -class ${className}Page extends StatelessWidget { - static const route = '/$module'; - const ${className}Page({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('$className')), - body: const Center(child: Text('$className — replace me')), - ); - } -} -'''; - -// ─── Test templates ─────────────────────────────────────────────────────────── - -String _tplInteractorTest(String module, String className) => ''' -import 'package:boilerplate/core/client/network_exception.dart'; -import 'package:boilerplate/features/$module/domain/${module}_interactor.dart'; -import 'package:boilerplate/features/$module/domain/model/$module.dart'; -import 'package:boilerplate/features/$module/domain/repository/${module}_repository.dart'; -import 'package:dartz/dartz.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -class Mock${className}Repository extends Mock implements ${className}Repository {} - -void main() { - late ${className}Interactor interactor; - late Mock${className}Repository mockRepository; - - setUp(() { - mockRepository = Mock${className}Repository(); - interactor = ${className}Interactor(mockRepository); - }); - - group('${className}Interactor', () { - test('getSomething returns data on success', () async { - when(() => mockRepository.getSomething()) - .thenAnswer((_) async => Right($className(id: 1))); - - final result = await interactor.getSomething(); - - expect(result.isRight(), true); - verify(() => mockRepository.getSomething()).called(1); - }); - - test('getSomething returns failure on error', () async { - when(() => mockRepository.getSomething()) - .thenAnswer((_) async => Left(NetworkException(message: 'error'))); - - final result = await interactor.getSomething(); - - expect(result.isLeft(), true); - }); - }); -} -'''; - -String _tplBlocTest(String module, String className) => ''' -import 'package:bloc_test/bloc_test.dart'; -import 'package:boilerplate/core/client/network_exception.dart'; -import 'package:boilerplate/features/$module/domain/model/$module.dart'; -import 'package:boilerplate/features/$module/domain/use_cases/${module}_use_cases.dart'; -import 'package:boilerplate/features/$module/presentation/blocs/${module}_bloc.dart'; -import 'package:boilerplate/features/$module/presentation/blocs/${module}_event.dart'; -import 'package:boilerplate/features/$module/presentation/blocs/${module}_state.dart'; -import 'package:dartz/dartz.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -class Mock${className}UseCases extends Mock implements ${className}UseCases {} - -void main() { - late Mock${className}UseCases mockUseCases; - - setUp(() { - mockUseCases = Mock${className}UseCases(); - }); - - group('${className}Bloc', () { - blocTest<${className}Bloc, ${className}State>( - 'emits [Loading, Success] when getSomething succeeds', - build: () => ${className}Bloc(mockUseCases), - setUp: () { - when(() => mockUseCases.getSomething()) - .thenAnswer((_) async => Right($className(id: 1))); - }, - act: (bloc) => bloc.add(const Get${className}Event()), - expect: () => [ - const ${className}LoadingState(), - isA<${className}SuccessState>(), - ], - ); - - blocTest<${className}Bloc, ${className}State>( - 'emits [Loading, Error] when getSomething fails', - build: () => ${className}Bloc(mockUseCases), - setUp: () { - when(() => mockUseCases.getSomething()) - .thenAnswer((_) async => Left(NetworkException(message: 'error'))); - }, - act: (bloc) => bloc.add(const Get${className}Event()), - expect: () => [ - const ${className}LoadingState(), - isA<${className}ErrorState>(), - ], - ); - }); -} -'''; - -String _tplCubitTest(String module, String className) => ''' -import 'package:bloc_test/bloc_test.dart'; -import 'package:boilerplate/core/client/network_exception.dart'; -import 'package:boilerplate/features/$module/domain/model/$module.dart'; -import 'package:boilerplate/features/$module/domain/use_cases/${module}_use_cases.dart'; -import 'package:boilerplate/features/$module/presentation/blocs/${module}_cubit.dart'; -import 'package:boilerplate/features/$module/presentation/blocs/${module}_state.dart'; -import 'package:dartz/dartz.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -class Mock${className}UseCases extends Mock implements ${className}UseCases {} - -void main() { - late Mock${className}UseCases mockUseCases; - - setUp(() { - mockUseCases = Mock${className}UseCases(); - }); - - group('${className}Cubit', () { - blocTest<${className}Cubit, ${className}State>( - 'emits [Loading, Success] when getSomething succeeds', - build: () => ${className}Cubit(mockUseCases), - setUp: () { - when(() => mockUseCases.getSomething()) - .thenAnswer((_) async => Right($className(id: 1))); - }, - act: (cubit) => cubit.getSomething(), - expect: () => [ - const ${className}LoadingState(), - isA<${className}SuccessState>(), - ], - ); - - blocTest<${className}Cubit, ${className}State>( - 'emits [Loading, Error] when getSomething fails', - build: () => ${className}Cubit(mockUseCases), - setUp: () { - when(() => mockUseCases.getSomething()) - .thenAnswer((_) async => Left(NetworkException(message: 'error'))); - }, - act: (cubit) => cubit.getSomething(), - expect: () => [ - const ${className}LoadingState(), - isA<${className}ErrorState>(), - ], - ); - }); -} -'''; - -String _tplRiverpodTest(String module, String className) => ''' -import 'package:boilerplate/core/client/network_exception.dart'; -import 'package:boilerplate/features/$module/domain/model/$module.dart'; -import 'package:boilerplate/features/$module/domain/use_cases/${module}_use_cases.dart'; -import 'package:boilerplate/features/$module/presentation/providers/${module}_notifier.dart'; -import 'package:boilerplate/features/$module/presentation/providers/${module}_state.dart'; -import 'package:dartz/dartz.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -class Mock${className}UseCases extends Mock implements ${className}UseCases {} - -void main() { - late Mock${className}UseCases mockUseCases; - - setUp(() { - mockUseCases = Mock${className}UseCases(); - }); - - ProviderContainer makeContainer() => ProviderContainer( - overrides: [ - ${module}UseCasesProvider.overrideWithValue(mockUseCases), - ], - ); - - group('${className}Notifier', () { - test('initial state is ${className}InitialState', () async { - when(() => mockUseCases.getSomething()) - .thenAnswer((_) async => Right($className(id: 1))); - - final container = makeContainer(); - addTearDown(container.dispose); - - final state = await container.read(${module}NotifierProvider.future); - expect(state, isA<${className}InitialState>()); - }); - }); -} -'''; - -// ─── spl.yaml helpers ───────────────────────────────────────────────────────── - -Map _readSplConfig() { - const path = 'spl.yaml'; - if (!File(path).existsSync()) _die('spl.yaml not found. Run from project root.'); - - final lines = File(path).readAsLinesSync(); - final config = {}; - String? section; - Map? currentFeature; - - for (final line in lines) { - if (line.trim().startsWith('#') || line.trim().isEmpty) continue; - - if (!line.startsWith(' ') && !line.startsWith('\t')) { - section = line.trim().replaceAll(':', ''); - if (section == 'features') config['features'] = >[]; - continue; - } - - final trimmed = line.trim(); - - if (section == 'app' || section == 'storage' || section == 'state_management') { - final idx = trimmed.indexOf(':'); - if (idx > 0) { - config.putIfAbsent(section!, () => {}); - (config[section] as Map)[trimmed.substring(0, idx).trim()] = - trimmed.substring(idx + 1).trim(); - } - } - - if (section == 'features') { - if (trimmed.startsWith('- name:')) { - currentFeature = {'name': trimmed.replaceFirst('- name:', '').trim()}; - (config['features'] as List).add(currentFeature); - } else if (currentFeature != null) { - final idx = trimmed.indexOf(':'); - if (idx > 0) { - currentFeature[trimmed.substring(0, idx).trim()] = - trimmed.substring(idx + 1).trim(); - } - } - } - } - - return config; -} - -void _addFeatureToConfig(String name, - {required String storage, required String state}) { - const path = 'spl.yaml'; - final content = File(path).readAsStringSync(); - File(path).writeAsStringSync( - '$content\n - name: $name\n status: active\n storage: $storage\n state: $state\n', - ); -} - -void _removeFeatureFromConfig(String name) { - const path = 'spl.yaml'; - final lines = File(path).readAsLinesSync(); - final result = []; - bool skip = false; - - for (final line in lines) { - if (line.trim() == '- name: $name') { - skip = true; - if (result.isNotEmpty && result.last.trim().isEmpty) result.removeLast(); - continue; - } - if (skip) { - if (line.trim().startsWith('- name:') || !line.startsWith(' ')) { - skip = false; - } else { - continue; - } - } - result.add(line); - } - File(path).writeAsStringSync(result.join('\n')); -} - -void _updateFeatureStatusInConfig(String name, String status) { - const path = 'spl.yaml'; - final lines = File(path).readAsLinesSync(); - final result = []; - bool inFeature = false; - bool patched = false; - - for (final line in lines) { - if (line.trim() == '- name: $name') { - inFeature = true; - patched = false; - } else if (inFeature && line.trim().startsWith('status:') && !patched) { - result.add(line.replaceFirst(RegExp(r'status:\s*\w+'), 'status: $status')); - patched = true; - continue; - } else if (inFeature && (line.trim().startsWith('- name:') || !line.startsWith(' '))) { - inFeature = false; - } - result.add(line); - } - File(path).writeAsStringSync(result.join('\n')); -} - -void _updateStorageInConfig(String provider) { - const path = 'spl.yaml'; - File(path).writeAsStringSync( - File(path).readAsStringSync().replaceFirst( - RegExp(r'local_backend:.*'), - 'local_backend: $provider', - ), - ); -} - -void _updateStateDefaultInConfig(String solution) { - const path = 'spl.yaml'; - File(path).writeAsStringSync( - File(path).readAsStringSync().replaceFirst( - RegExp(r'default: (bloc|cubit|riverpod)'), - 'default: $solution', - ), - ); -} - -// ─── Mason integration ──────────────────────────────────────────────────────── - -bool? _masonAvailable; - -Future _checkMason() async { - if (_masonAvailable != null) return _masonAvailable!; - final r = await Process.run('mason', ['--version'], runInShell: true); - _masonAvailable = r.exitCode == 0 && File('.mason/bricks.json').existsSync(); - return _masonAvailable!; -} - -Future _tryMasonFeature(String module, - {bool withStorage = false, String state = 'bloc'}) async { - if (!await _checkMason()) return false; - print(' Using Mason brick: feature'); - final r = await Process.run( - 'mason', - ['make', 'feature', - '--name', module, - '--with_storage', withStorage.toString(), - '--state', state, - '-o', '.', '--no-confirm'], - runInShell: true, - ); - if (r.exitCode != 0) { - print(' Mason failed → falling back to inline templates.'); - return false; - } - print(r.stdout); - return true; -} - -Future _tryMasonStorage(String provider) async { - if (!await _checkMason()) return false; - final brick = switch (provider) { - 'flutter_secure_storage' => 'storage_secure', - 'sqflite' => 'storage_sqflite', - 'hive' => 'storage_hive', - 'shared_preferences' => 'storage_prefs', - _ => null, - }; - if (brick == null) return false; - print(' Using Mason brick: $brick'); - final r = await Process.run( - 'mason', ['make', brick, '-o', '.', '--no-confirm'], - runInShell: true, - ); - if (r.exitCode != 0) { - print(' Mason failed → falling back to inline templates.'); - return false; - } - print(r.stdout); - return true; -} - -// ─── Validation + Notes ─────────────────────────────────────────────────────── - -void _validateStateChoice(String state) { - const valid = ['bloc', 'cubit', 'riverpod']; - if (!valid.contains(state)) { - _die('Unknown state: "$state"\nValid: ${valid.join(' | ')}'); - } -} - -void _printStateNotes(String state) { - if (state == 'riverpod') { - print(''); - print(' ⚠ Riverpod requires: flutter_riverpod in pubspec.yaml'); - print(' ⚠ Add ProviderScope at the root of your widget tree in main()'); - } -} - -void _printStorageNotes(String provider) { - switch (provider) { - case 'hive': - print('\n ⚠ Add: hive_flutter: ^1.1.0 to pubspec.yaml'); - print(' ⚠ Call di().init() in main() before runApp()'); - case 'shared_preferences': - print('\n ⚠ Add: shared_preferences: ^2.3.0 to pubspec.yaml'); - print(' ⚠ Call di().init() in main() before runApp()'); - case 'sqflite': - print('\n ⚠ Call di().init() in main() before runApp()'); - default: - break; - } -} - -// ─── build_runner ───────────────────────────────────────────────────────────── - -Future _runBuildRunner() async { - final result = await Process.run( - 'dart', - ['run', 'build_runner', 'build', '--delete-conflicting-outputs'], - runInShell: true, - ); - if (result.exitCode != 0) { - print('\n${result.stderr}'); - _die('build_runner failed (exit ${result.exitCode})'); - } - print(' build_runner: OK'); -} - -// ─── Utilities ──────────────────────────────────────────────────────────────── - -String _toPascalCase(String s) => s - .split(RegExp(r'[_\s-]+')) - .map((w) => w.isEmpty ? '' : '${w[0].toUpperCase()}${w.substring(1)}') - .join(); - -void _printHeader(String t) { - print(''); - print(' ══ $t ══'); - print(''); -} - -void _printHelp() { - print(''' -SPL Manager — Software Product Line CLI - -Variability: - Storage [XOR] one backend for the whole app - State Mgmt [OR] global default + per-feature override - -Commands: - list - add Scaffold a new feature (active) - add --with-storage Include local cache (AppStorage) - add --with-test Generate unit + state mgmt tests - add --shell-route Register as shell (bottom nav) route - add --state bloc|cubit|riverpod Override state mgmt for this feature - disable Move to catalog — code kept, DI removed - enable Restore from catalog — DI re-wired - remove [--yes|-y] Hard delete (active or catalog) - storage set Switch storage (XOR) - storage list - state set Change default state mgmt - state list - fix Re-run build_runner -'''); -} - -Never _die(String msg) { - stderr.writeln('\n ✗ $msg\n'); - exit(1); -} diff --git a/codegen/src/commands.dart b/codegen/src/commands.dart new file mode 100644 index 0000000..f49c1f0 --- /dev/null +++ b/codegen/src/commands.dart @@ -0,0 +1,305 @@ +// ignore_for_file: avoid_print +part of '../spl_manager.dart'; + +// ─── Commands ───────────────────────────────────────────────────────────────── + +Future _cmdList() async { + final config = _readSplConfig(); + _printHeader('SPL Configuration'); + + final storage = config['storage']?['local_backend'] ?? 'flutter_secure_storage'; + final stateDefault = config['state_management']?['default'] ?? 'bloc'; + + print(' App : ${config['app']?['name'] ?? 'unknown'}'); + print(' Storage [XOR] : $storage'); + print(' State Mgmt [OR] : $stateDefault (default, per-feature override allowed)'); + print(''); + + final features = config['features'] as List>? ?? []; + if (features.isEmpty) { + print(' No features yet.'); + print(' dart run codegen/spl_manager.dart add '); + return; + } + + final active = features.where((f) => (f['status'] ?? 'active') == 'active').toList(); + final inactive = features.where((f) => (f['status'] ?? 'active') == 'inactive').toList(); + + print(' Active features [compiled + DI-wired]:'); + if (active.isEmpty) { + print(' (none)'); + } else { + for (final f in active) { + final name = f['name'] ?? '?'; + final storage = f['storage'] ?? 'none'; + final state = f['state'] ?? stateDefault; + final desc = f['description'] ?? ''; + final storageTag = storage == 'none' ? '' : ' storage:$storage'; + print(' ✓ $name state:$state$storageTag'); + if (desc.isNotEmpty && desc != '""') print(' $desc'); + } + } + + if (inactive.isNotEmpty) { + print(''); + print(' Catalog [code preserved, not compiled]:'); + for (final f in inactive) { + final name = f['name'] ?? '?'; + final storage = f['storage'] ?? 'none'; + final state = f['state'] ?? stateDefault; + final desc = f['description'] ?? ''; + final storageTag = storage == 'none' ? '' : ' storage:$storage'; + print(' ○ $name state:$state$storageTag'); + if (desc.isNotEmpty && desc != '""') print(' $desc'); + } + } + + print(''); + print(' Tip: dart run codegen/spl_manager.dart storage list'); + print(' dart run codegen/spl_manager.dart state list'); +} + +Future _cmdAdd( + String name, { + bool withStorage = false, + bool withTest = false, + bool shellRoute = false, + String? stateOverride, +}) async { + final module = name.toLowerCase().replaceAll(RegExp(r'[^a-z0-9_]'), '_'); + final className = _toPascalCase(module); + final featureDir = 'lib/features/$module'; + + if (Directory(featureDir).existsSync()) { + _die('Feature "$module" already exists at $featureDir'); + } + if (Directory('features_catalog/$module').existsSync()) { + _die('Feature "$module" exists in the catalog (disabled).\n' + ' To restore it: dart run codegen/spl_manager.dart enable $module\n' + ' To delete it: dart run codegen/spl_manager.dart remove $module'); + } + + final config = _readSplConfig(); + final globalStorageBackend = config['storage']?['local_backend'] ?? 'flutter_secure_storage'; + final globalStateDefault = config['state_management']?['default'] ?? 'bloc'; + final stateChoice = stateOverride ?? globalStateDefault; + + _validateStateChoice(stateChoice); + + _printHeader('Adding feature: $module'); + print(' Class : $className'); + print(' Storage : ${withStorage ? globalStorageBackend : 'none'}'); + print(' State : $stateChoice${stateOverride != null ? ' (override)' : ' (default)'}'); + print(' Route : ${shellRoute ? 'shell (bottom nav)' : 'top-level'}'); + print(' Tests : ${withTest ? 'yes (--with-test)' : 'no'}'); + print(' DI : auto-wired via build_runner (@injectable)'); + print(''); + + final usedMason = await _tryMasonFeature(module, + withStorage: withStorage, state: stateChoice); + if (!usedMason) { + _generateFeatureFiles(module, className, + withStorage: withStorage, state: stateChoice); + } + + _addFeatureToConfig( + module, + storage: withStorage ? globalStorageBackend : 'none', + state: stateChoice, + ); + + _injectRoute(module, className, shellRoute: shellRoute); + + if (withTest) _generateTestFiles(module, className, state: stateChoice); + + _printStateNotes(stateChoice); + + print('\n Wiring DI (build_runner)...'); + await _runBuildRunner(); + + print('\n ✓ Done! lib/features/$module/'); +} + +Future _cmdDisable(String name) async { + final module = name.toLowerCase(); + final activeDir = 'lib/features/$module'; + final catalogDir = 'features_catalog/$module'; + + _printHeader('Disabling feature: $module'); + + if (!Directory(activeDir).existsSync()) { + if (Directory(catalogDir).existsSync()) { + _die('Feature "$module" is already disabled (in catalog).'); + } + _die('Feature "$module" not found.'); + } + + Directory('features_catalog').createSync(); + Directory(activeDir).renameSync(catalogDir); + print(' ○ Moved: $activeDir → $catalogDir'); + + _removeRoute(module); + _updateFeatureStatusInConfig(module, 'inactive'); + + print(' Regenerating DI...'); + await _runBuildRunner(); + print('\n ✓ Feature "$module" disabled.'); + print(' → Restore with: dart run codegen/spl_manager.dart enable $module'); +} + +Future _cmdEnable(String name) async { + final module = name.toLowerCase(); + final activeDir = 'lib/features/$module'; + final catalogDir = 'features_catalog/$module'; + + _printHeader('Enabling feature: $module'); + + if (!Directory(catalogDir).existsSync()) { + if (Directory(activeDir).existsSync()) { + _die('Feature "$module" is already active.'); + } + _die('Feature "$module" not found in catalog.\n' + ' Add it fresh: dart run codegen/spl_manager.dart add $module'); + } + + Directory('lib/features').createSync(recursive: true); + Directory(catalogDir).renameSync(activeDir); + print(' ✓ Moved: $catalogDir → $activeDir'); + + _updateFeatureStatusInConfig(module, 'active'); + + print(' Wiring DI (build_runner)...'); + await _runBuildRunner(); + print('\n ✓ Feature "$module" enabled.'); + print(' → Ensure route is registered in lib/core/router/app_router_config.dart'); +} + +Future _cmdRemove(String name, {bool force = false}) async { + final module = name.toLowerCase(); + final activeDir = 'lib/features/$module'; + final catalogDir = 'features_catalog/$module'; + + final inActive = Directory(activeDir).existsSync(); + final inCatalog = Directory(catalogDir).existsSync(); + + if (!inActive && !inCatalog) _die('Feature "$module" not found.'); + + final location = inActive ? activeDir : catalogDir; + _printHeader('Removing feature: $module'); + print(' Location: $location${inCatalog ? ' (disabled)' : ' (active)'}'); + + if (!force) { + stdout.write(' Permanently delete "$module"? [y/N] '); + final confirm = stdin.readLineSync()?.toLowerCase(); + if (confirm != 'y' && confirm != 'yes') { print(' Aborted.'); exit(0); } + } + + Directory(location).deleteSync(recursive: true); + print(' Deleted: $location'); + _removeRoute(module); + _removeTests(module); + _removeFeatureFromConfig(module); + + if (inActive) { + print(' Regenerating DI...'); + await _runBuildRunner(); + } + print('\n ✓ Feature "$module" permanently removed.'); +} + +Future _cmdStorageSet(String provider) async { + const valid = ['flutter_secure_storage', 'sqflite', 'hive', 'shared_preferences']; + if (!valid.contains(provider)) { + _die('Unknown provider: "$provider"\nValid: ${valid.join(' | ')}'); + } + + final current = _getActiveProviderName(); + if (current == provider) { print('\n Already using "$provider".'); exit(0); } + + _printHeader('Switching storage [XOR]: $current → $provider'); + + _deleteStorageImpl(current); + + final usedMason = await _tryMasonStorage(provider); + if (!usedMason) _generateStorageImpl(provider); + + _rewriteStorageModule(provider); + _updateStorageInConfig(provider); + + print('\n Regenerating DI...'); + await _runBuildRunner(); + + print('\n ✓ Storage → "$provider"'); + _printStorageNotes(provider); +} + +void _cmdStorageList() { + _printHeader('Storage Providers [XOR — exactly one active]'); + final current = _getActiveProviderName(); + final providers = { + 'flutter_secure_storage': 'Encrypted key-value. Strings only. Best for sensitive data.', + 'sqflite': 'SQLite (relational). Best for structured/queryable data.', + 'hive': 'NoSQL box store. Fast reads. Best for object graphs.', + 'shared_preferences': 'Simple key-value. Non-encrypted. Best for user settings.', + }; + for (final e in providers.entries) { + final active = e.key == current ? ' ◀ active' : ''; + print(' ${e.key}$active'); + print(' ${e.value}'); + print(''); + } + print(' Switch (XOR): dart run codegen/spl_manager.dart storage set '); +} + +void _cmdStateSet(String solution) { + _validateStateChoice(solution); + _updateStateDefaultInConfig(solution); + _printHeader('State Management Default → $solution'); + print(' Updated spl.yaml default.'); + print(' Existing features are unchanged.'); + print(' New features will use: $solution'); + _printStateNotes(solution); +} + +void _cmdStateList() { + _printHeader('State Management [OR — global default + per-feature override]'); + final config = _readSplConfig(); + final current = config['state_management']?['default'] ?? 'bloc'; + + final solutions = { + 'bloc': [ + 'flutter_bloc (already in pubspec)', + 'Event + State + Bloc. Explicit event stream. Best for complex flows.', + 'Files: _event.dart _state.dart _bloc.dart', + ], + 'cubit': [ + 'flutter_bloc (already in pubspec, same package as bloc)', + 'State + Cubit only. No event classes. Simpler, fewer files.', + 'Files: _state.dart _cubit.dart', + ], + 'riverpod': [ + 'flutter_riverpod (add to pubspec if not present)', + 'Notifier + Provider. Different DI model. Bridges to get_it via di().', + 'Files: _state.dart _notifier.dart', + ], + }; + + for (final e in solutions.entries) { + final active = e.key == current ? ' ◀ default' : ''; + print(' ${e.key}$active'); + for (final line in e.value) print(' $line'); + print(''); + } + + print(' Change default : dart run codegen/spl_manager.dart state set '); + print(' Per-feature : dart run codegen/spl_manager.dart add --state '); + print(''); + print(' Note: bloc and cubit coexist freely (same package).'); + print(' riverpod requires flutter_riverpod in pubspec.yaml.'); +} + +Future _cmdFix() async { + _printHeader('Running build_runner'); + await _runBuildRunner(); + print(' ✓ Done'); +} diff --git a/codegen/src/generators.dart b/codegen/src/generators.dart new file mode 100644 index 0000000..45dc91a --- /dev/null +++ b/codegen/src/generators.dart @@ -0,0 +1,218 @@ +// ignore_for_file: avoid_print +part of '../spl_manager.dart'; + +// ─── Feature file generation ────────────────────────────────────────────────── + +void _generateFeatureFiles( + String module, + String className, { + bool withStorage = false, + String state = 'bloc', +}) { + final dirs = [ + 'lib/features/$module/data/local', + 'lib/features/$module/data/model/mapper', + 'lib/features/$module/data/model/responses', + 'lib/features/$module/data/remote', + 'lib/features/$module/domain/model', + 'lib/features/$module/domain/repository', + 'lib/features/$module/domain/use_cases', + if (state == 'riverpod') + 'lib/features/$module/presentation/providers' + else + 'lib/features/$module/presentation/blocs', + 'lib/features/$module/presentation/pages', + 'lib/features/$module/presentation/widgets', + ]; + for (final d in dirs) Directory(d).createSync(recursive: true); + + final files = { + // Data layer + 'lib/features/$module/data/local/${module}_local_data_sources.dart': + _tplLocalDataSources(module, className, withStorage: withStorage), + 'lib/features/$module/data/model/mapper/${module}_mapper.dart': + _tplMapper(module, className), + 'lib/features/$module/data/model/responses/${module}_response.dart': + _tplResponse(module, className), + 'lib/features/$module/data/remote/${module}_remote_data_sources.dart': + _tplRemoteDataSources(module, className), + 'lib/features/$module/data/${module}_repository_impl.dart': + _tplRepositoryImpl(module, className), + // Domain layer + 'lib/features/$module/domain/model/$module.dart': _tplModel(className), + 'lib/features/$module/domain/repository/${module}_repository.dart': + _tplRepository(module, className), + 'lib/features/$module/domain/use_cases/${module}_use_cases.dart': + _tplUseCases(module, className), + 'lib/features/$module/domain/${module}_interactor.dart': + _tplInteractor(module, className), + // Presentation — page (always the same) + 'lib/features/$module/presentation/pages/${module}_page.dart': + _tplPage(module, className), + }; + + // Presentation — state management varies + files.addAll(_stateFiles(module, className, state)); + + for (final e in files.entries) { + File(e.key).writeAsStringSync(e.value); + print(' + ${e.key}'); + } +} + +Map _stateFiles(String module, String className, String state) { + switch (state) { + case 'cubit': + return { + 'lib/features/$module/presentation/blocs/${module}_state.dart': + _tplState(className), + 'lib/features/$module/presentation/blocs/${module}_cubit.dart': + _tplCubit(module, className), + }; + case 'riverpod': + return { + 'lib/features/$module/presentation/providers/${module}_state.dart': + _tplState(className), + 'lib/features/$module/presentation/providers/${module}_notifier.dart': + _tplRiverpodNotifier(module, className), + }; + default: // bloc + return { + 'lib/features/$module/presentation/blocs/${module}_event.dart': + _tplEvent(className), + 'lib/features/$module/presentation/blocs/${module}_state.dart': + _tplState(className), + 'lib/features/$module/presentation/blocs/${module}_bloc.dart': + _tplBloc(module, className), + }; + } +} + +// ─── Route injection ────────────────────────────────────────────────────────── + +void _injectRoute(String module, String className, {bool shellRoute = false}) { + const routerPath = 'lib/core/router/app_router_config.dart'; + if (!File(routerPath).existsSync()) { + print(' ⚠ Router not found at $routerPath — skipping route injection.'); + print(' Register the route manually.'); + return; + } + + var content = File(routerPath).readAsStringSync(); + final pageImport = + "import 'package:boilerplate/features/$module/presentation/pages/${module}_page.dart';"; + + if (content.contains('${className}Page.route')) { + print(' ~ Route for $className already exists — skipping.'); + return; + } + + // Add import — insert before 'import package:flutter' + content = content.replaceFirst( + "import 'package:flutter/", + "$pageImport\nimport 'package:flutter/", + ); + + if (shellRoute) { + // Find the last GoRoute inside ShellRoute and append after it + const anchor = "builder: (context, state) => const ProfilePage())"; + final newEntry = "\n GoRoute(\n" + " path: ${className}Page.route,\n" + " name: ${className}Page.route,\n" + " parentNavigatorKey: _shellKey,\n" + " pageBuilder: (context, state) =>\n" + " const NoTransitionPage(child: ${className}Page()),\n" + " builder: (context, state) => const ${className}Page())"; + content = content.replaceFirst(anchor, '$anchor$newEntry'); + } else { + // Insert top-level GoRoute before ShellRoute( + const anchor = ' ShellRoute('; + final newEntry = " GoRoute(\n" + " path: ${className}Page.route,\n" + " name: ${className}Page.route,\n" + " builder: (context, state) => const ${className}Page()),\n"; + content = content.replaceFirst(anchor, '$newEntry ShellRoute('); + } + + File(routerPath).writeAsStringSync(content); + print(' ~ lib/core/router/app_router_config.dart (route injected)'); +} + +void _removeRoute(String module) { + const routerPath = 'lib/core/router/app_router_config.dart'; + if (!File(routerPath).existsSync()) return; + + final className = _toPascalCase(module); + var content = File(routerPath).readAsStringSync(); + final before = content.length; + + // Remove the import line + content = content.replaceAll( + "import 'package:boilerplate/features/$module/presentation/pages/${module}_page.dart';\n", + '', + ); + + // Remove top-level GoRoute (exact format we generate) + content = content.replaceAll( + " GoRoute(\n" + " path: ${className}Page.route,\n" + " name: ${className}Page.route,\n" + " builder: (context, state) => const ${className}Page()),\n", + '', + ); + + // Remove shell GoRoute (exact format we generate) + content = content.replaceAll( + "\n GoRoute(\n" + " path: ${className}Page.route,\n" + " name: ${className}Page.route,\n" + " parentNavigatorKey: _shellKey,\n" + " pageBuilder: (context, state) =>\n" + " const NoTransitionPage(child: ${className}Page()),\n" + " builder: (context, state) => const ${className}Page())", + '', + ); + + if (content.length != before) { + File(routerPath).writeAsStringSync(content); + print(' ~ lib/core/router/app_router_config.dart (route removed)'); + } +} + +void _removeTests(String module) { + final testDir = Directory('test/features/$module'); + if (testDir.existsSync()) { + testDir.deleteSync(recursive: true); + print(' Deleted: test/features/$module'); + } +} + +// ─── Test file generation ───────────────────────────────────────────────────── + +void _generateTestFiles(String module, String className, {String state = 'bloc'}) { + final testDir = 'test/features/$module'; + Directory('$testDir/domain').createSync(recursive: true); + Directory('$testDir/presentation').createSync(recursive: true); + + final files = { + '$testDir/domain/${module}_interactor_test.dart': + _tplInteractorTest(module, className), + }; + + switch (state) { + case 'cubit': + files['$testDir/presentation/${module}_cubit_test.dart'] = + _tplCubitTest(module, className); + case 'riverpod': + files['$testDir/presentation/${module}_notifier_test.dart'] = + _tplRiverpodTest(module, className); + default: // bloc + files['$testDir/presentation/${module}_bloc_test.dart'] = + _tplBlocTest(module, className); + } + + for (final e in files.entries) { + File(e.key).writeAsStringSync(e.value); + print(' + ${e.key}'); + } +} diff --git a/codegen/src/spl_config.dart b/codegen/src/spl_config.dart new file mode 100644 index 0000000..68b54e1 --- /dev/null +++ b/codegen/src/spl_config.dart @@ -0,0 +1,181 @@ +// ignore_for_file: avoid_print +part of '../spl_manager.dart'; + +// ─── spl.yaml helpers ───────────────────────────────────────────────────────── + +Map _readSplConfig() { + const path = 'spl.yaml'; + if (!File(path).existsSync()) _die('spl.yaml not found. Run from project root.'); + + final lines = File(path).readAsLinesSync(); + final config = {}; + String? section; + Map? currentFeature; + + for (final line in lines) { + if (line.trim().startsWith('#') || line.trim().isEmpty) continue; + + if (!line.startsWith(' ') && !line.startsWith('\t')) { + section = line.trim().replaceAll(':', ''); + if (section == 'features') config['features'] = >[]; + continue; + } + + final trimmed = line.trim(); + + if (section == 'app' || section == 'storage' || section == 'state_management') { + final idx = trimmed.indexOf(':'); + if (idx > 0) { + config.putIfAbsent(section!, () => {}); + (config[section] as Map)[trimmed.substring(0, idx).trim()] = + trimmed.substring(idx + 1).trim(); + } + } + + if (section == 'features') { + if (trimmed.startsWith('- name:')) { + currentFeature = {'name': trimmed.replaceFirst('- name:', '').trim()}; + (config['features'] as List).add(currentFeature); + } else if (currentFeature != null) { + final idx = trimmed.indexOf(':'); + if (idx > 0) { + currentFeature[trimmed.substring(0, idx).trim()] = + trimmed.substring(idx + 1).trim(); + } + } + } + } + + return config; +} + +void _addFeatureToConfig(String name, + {required String storage, required String state}) { + const path = 'spl.yaml'; + final content = File(path).readAsStringSync(); + File(path).writeAsStringSync( + '$content\n - name: $name\n status: active\n storage: $storage\n state: $state\n', + ); +} + +void _removeFeatureFromConfig(String name) { + const path = 'spl.yaml'; + final lines = File(path).readAsLinesSync(); + final result = []; + bool skip = false; + + for (final line in lines) { + if (line.trim() == '- name: $name') { + skip = true; + if (result.isNotEmpty && result.last.trim().isEmpty) result.removeLast(); + continue; + } + if (skip) { + if (line.trim().startsWith('- name:') || !line.startsWith(' ')) { + skip = false; + } else { + continue; + } + } + result.add(line); + } + File(path).writeAsStringSync(result.join('\n')); +} + +void _updateFeatureStatusInConfig(String name, String status) { + const path = 'spl.yaml'; + final lines = File(path).readAsLinesSync(); + final result = []; + bool inFeature = false; + bool patched = false; + + for (final line in lines) { + if (line.trim() == '- name: $name') { + inFeature = true; + patched = false; + } else if (inFeature && line.trim().startsWith('status:') && !patched) { + result.add(line.replaceFirst(RegExp(r'status:\s*\w+'), 'status: $status')); + patched = true; + continue; + } else if (inFeature && (line.trim().startsWith('- name:') || !line.startsWith(' '))) { + inFeature = false; + } + result.add(line); + } + File(path).writeAsStringSync(result.join('\n')); +} + +void _updateStorageInConfig(String provider) { + const path = 'spl.yaml'; + File(path).writeAsStringSync( + File(path).readAsStringSync().replaceFirst( + RegExp(r'local_backend:.*'), + 'local_backend: $provider', + ), + ); +} + +void _updateStateDefaultInConfig(String solution) { + const path = 'spl.yaml'; + File(path).writeAsStringSync( + File(path).readAsStringSync().replaceFirst( + RegExp(r'default: (bloc|cubit|riverpod)'), + 'default: $solution', + ), + ); +} + +// ─── Mason integration ──────────────────────────────────────────────────────── + +bool? _masonAvailable; + +Future _checkMason() async { + if (_masonAvailable != null) return _masonAvailable!; + final r = await Process.run('mason', ['--version'], runInShell: true); + _masonAvailable = r.exitCode == 0 && File('.mason/bricks.json').existsSync(); + return _masonAvailable!; +} + +Future _tryMasonFeature(String module, + {bool withStorage = false, String state = 'bloc'}) async { + if (!await _checkMason()) return false; + print(' Using Mason brick: feature'); + final r = await Process.run( + 'mason', + ['make', 'feature', + '--name', module, + '--with_storage', withStorage.toString(), + '--state', state, + '-o', '.', '--no-confirm'], + runInShell: true, + ); + if (r.exitCode != 0) { + print(' Mason failed → falling back to inline templates.'); + return false; + } + print(r.stdout); + return true; +} + +Future _tryMasonStorage(String provider) async { + if (!await _checkMason()) return false; + final brick = switch (provider) { + 'flutter_secure_storage' => 'storage_secure', + 'sqflite' => 'storage_sqflite', + 'hive' => 'storage_hive', + 'shared_preferences' => 'storage_prefs', + _ => null, + }; + if (brick == null) return false; + print(' Using Mason brick: $brick'); + final r = await Process.run( + 'mason', ['make', brick, '-o', '.', '--no-confirm'], + runInShell: true, + ); + if (r.exitCode != 0) { + print(' Mason failed → falling back to inline templates.'); + return false; + } + print(r.stdout); + return true; +} diff --git a/codegen/src/storage_manager.dart b/codegen/src/storage_manager.dart new file mode 100644 index 0000000..1355de7 --- /dev/null +++ b/codegen/src/storage_manager.dart @@ -0,0 +1,83 @@ +// ignore_for_file: avoid_print +part of '../spl_manager.dart'; + +// ─── Storage impl management ────────────────────────────────────────────────── + +String _getActiveProviderName() { + const path = 'lib/core/storage/storage_module.dart'; + if (!File(path).existsSync()) return 'flutter_secure_storage'; + final content = File(path).readAsStringSync(); + final match = RegExp(r'// Active provider: (\S+)').firstMatch(content); + return match?.group(1)?.trim() ?? 'flutter_secure_storage'; +} + +String _implFileName(String provider) => switch (provider) { + 'flutter_secure_storage' => 'secure_storage_provider.dart', + 'sqflite' => 'sqflite_storage_provider.dart', + 'hive' => 'hive_storage_provider.dart', + 'shared_preferences' => 'shared_prefs_storage_provider.dart', + _ => _die('Unknown provider: $provider'), +}; + +String _implFilePath(String p) => 'lib/core/storage/impl/${_implFileName(p)}'; + +void _deleteStorageImpl(String provider) { + final path = _implFilePath(provider); + if (File(path).existsSync()) { + File(path).deleteSync(); + print(' - $path (removed)'); + } +} + +void _generateStorageImpl(String provider) { + final path = _implFilePath(provider); + File(path).writeAsStringSync(_storageImplContent(provider)); + print(' + $path (generated)'); +} + +String _storageImplContent(String provider) => switch (provider) { + 'flutter_secure_storage' => _tplSecureStorageProvider(), + 'sqflite' => _tplSqfliteProvider(), + 'hive' => _tplHiveProvider(), + 'shared_preferences' => _tplSharedPrefsProvider(), + _ => _die('Unknown provider: $provider'), +}; + +void _rewriteStorageModule(String provider) { + final imports = switch (provider) { + 'flutter_secure_storage' => + "import 'package:flutter_secure_storage/flutter_secure_storage.dart';\nimport 'impl/secure_storage_provider.dart';", + 'sqflite' => "import 'impl/sqflite_storage_provider.dart';", + 'hive' => "import 'impl/hive_storage_provider.dart';", + 'shared_preferences' => "import 'impl/shared_prefs_storage_provider.dart';", + _ => _die('Unknown provider: $provider'), + }; + final providerExpr = switch (provider) { + 'flutter_secure_storage' => 'const SecureStorageProvider(FlutterSecureStorage())', + 'sqflite' => 'SqfliteStorageProvider()', + 'hive' => 'HiveStorageProvider()', + 'shared_preferences' => 'SharedPrefsStorageProvider()', + _ => _die('Unknown provider: $provider'), + }; + + const path = 'lib/core/storage/storage_module.dart'; + File(path).writeAsStringSync('''// ============================================================ +// SPL MANAGED FILE — DO NOT EDIT MANUALLY +// Active provider: $provider +// To switch: dart run codegen/spl_manager.dart storage set +// Available: flutter_secure_storage | sqflite | hive | shared_preferences +// ============================================================ + +$imports + +import 'package:injectable/injectable.dart'; +import 'app_storage.dart'; + +@module +abstract class StorageModule { + @lazySingleton + AppStorage get appStorage => $providerExpr; +} +'''); + print(' ~ lib/core/storage/storage_module.dart (updated)'); +} diff --git a/codegen/src/templates.dart b/codegen/src/templates.dart new file mode 100644 index 0000000..ede5adb --- /dev/null +++ b/codegen/src/templates.dart @@ -0,0 +1,665 @@ +// ignore_for_file: avoid_print +part of '../spl_manager.dart'; + +// ─── Code templates — State Management ─────────────────────────────────────── + +String _tplState(String className) => ''' +import 'package:equatable/equatable.dart'; + +abstract class ${className}State extends Equatable { + const ${className}State(); + @override + List get props => []; +} + +class ${className}InitialState extends ${className}State { + const ${className}InitialState(); +} + +class ${className}LoadingState extends ${className}State { + const ${className}LoadingState(); +} + +class ${className}SuccessState extends ${className}State { + final dynamic data; + const ${className}SuccessState({required this.data}); + @override + List get props => [data]; +} + +class ${className}ErrorState extends ${className}State { + final String message; + const ${className}ErrorState({required this.message}); + @override + List get props => [message]; +} +'''; + +// ── BLoC ────────────────────────────────────────────────────────────────────── + +String _tplEvent(String className) => ''' +import 'package:equatable/equatable.dart'; + +abstract class ${className}Event extends Equatable { + const ${className}Event(); + @override + List get props => []; +} + +class Get${className}Event extends ${className}Event { + const Get${className}Event(); +} +'''; + +String _tplBloc(String module, String className) => ''' +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; + +import '../../domain/use_cases/${module}_use_cases.dart'; +import '${module}_event.dart'; +import '${module}_state.dart'; + +@Injectable() +class ${className}Bloc extends Bloc<${className}Event, ${className}State> { + final ${className}UseCases _useCases; + + ${className}Bloc(this._useCases) : super(const ${className}InitialState()) { + on(_onGet); + } + + Future _onGet( + Get${className}Event event, + Emitter<${className}State> emit, + ) async { + emit(const ${className}LoadingState()); + final result = await _useCases.getSomething(); + result.fold( + (failure) => emit(${className}ErrorState(message: failure.message ?? '')), + (data) => emit(${className}SuccessState(data: data)), + ); + } +} +'''; + +// ── Cubit ───────────────────────────────────────────────────────────────────── + +String _tplCubit(String module, String className) => ''' +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; + +import '../../domain/use_cases/${module}_use_cases.dart'; +import '${module}_state.dart'; + +// Cubit: no event classes needed. Call methods directly from UI. +// Uses flutter_bloc — same package as Bloc, no extra dependency. +@Injectable() +class ${className}Cubit extends Cubit<${className}State> { + final ${className}UseCases _useCases; + + ${className}Cubit(this._useCases) : super(const ${className}InitialState()); + + Future getSomething() async { + emit(const ${className}LoadingState()); + final result = await _useCases.getSomething(); + result.fold( + (failure) => emit(${className}ErrorState(message: failure.message ?? '')), + (data) => emit(${className}SuccessState(data: data)), + ); + } +} +'''; + +// ── Riverpod ────────────────────────────────────────────────────────────────── + +String _tplRiverpodNotifier(String module, String className) => ''' +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../services/di.dart'; +import '../../domain/use_cases/${module}_use_cases.dart'; +import '${module}_state.dart'; + +// Bridges injectable get_it DI → Riverpod. +// The domain/data layers stay injectable; only the presentation uses Riverpod. +final ${module}UseCasesProvider = Provider<${className}UseCases>( + (ref) => di<${className}UseCases>(), +); + +final ${module}NotifierProvider = + AsyncNotifierProvider.autoDispose<${className}Notifier, ${className}State>( + ${className}Notifier.new, +); + +class ${className}Notifier + extends AutoDisposeAsyncNotifier<${className}State> { + late ${className}UseCases _useCases; + + @override + Future<${className}State> build() async { + _useCases = ref.read(${module}UseCasesProvider); + return const ${className}InitialState(); + } + + Future getSomething() async { + state = const AsyncValue.loading(); + final result = await _useCases.getSomething(); + result.fold( + (failure) => state = + AsyncError(failure.message ?? 'Error', StackTrace.current), + (data) => state = AsyncData(${className}SuccessState(data: data)), + ); + } +} +'''; + +// ─── Storage provider templates ─────────────────────────────────────────────── + +String _tplSecureStorageProvider() => r''' +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import '../app_storage.dart'; + +/// [AppStorage] backed by FlutterSecureStorage. +/// Managed by spl_manager. To switch: dart run codegen/spl_manager.dart storage set +class SecureStorageProvider implements AppStorage { + final FlutterSecureStorage _storage; + const SecureStorageProvider(this._storage); + + @override Future init() async {} + + @override + Future put(String key, dynamic value) async => + _storage.write(key: key, value: value.toString()); + + @override + Future get(String key) async { + final value = await _storage.read(key: key); + if (value == null) return null; + if (T == int) return int.tryParse(value) as T?; + if (T == double) return double.tryParse(value) as T?; + if (T == bool) return (value == 'true') as T?; + return value as T?; + } + + @override Future delete(String key) async => _storage.delete(key: key); + @override Future clear() async => _storage.deleteAll(); + @override Future contains(String key) async => + _storage.containsKey(key: key); +} +'''; + +String _tplSqfliteProvider() => r''' +import 'package:sqflite/sqflite.dart'; +import '../app_storage.dart'; + +/// [AppStorage] backed by sqflite. +/// Call di().init() in main() before runApp(). +class SqfliteStorageProvider implements AppStorage { + Database? _db; + static const _table = 'kv_store'; + + @override + Future init() async { + final path = await getDatabasesPath(); + _db = await openDatabase( + '$path/app_storage.db', + version: 1, + onCreate: (db, _) async => db.execute( + 'CREATE TABLE $_table (key TEXT PRIMARY KEY, value TEXT NOT NULL)', + ), + ); + } + + @override + Future put(String key, dynamic value) async => + _db!.insert(_table, {'key': key, 'value': value.toString()}, + conflictAlgorithm: ConflictAlgorithm.replace); + + @override + Future get(String key) async { + final rows = await _db!.query(_table, where: 'key = ?', whereArgs: [key]); + if (rows.isEmpty) return null; + final raw = rows.first['value'] as String; + if (T == int) return int.tryParse(raw) as T?; + if (T == double) return double.tryParse(raw) as T?; + if (T == bool) return (raw == 'true') as T?; + return raw as T?; + } + + @override Future delete(String key) async => + _db!.delete(_table, where: 'key = ?', whereArgs: [key]); + @override Future clear() async => _db!.delete(_table); + @override Future contains(String key) async { + final rows = await _db!.query(_table, where: 'key = ?', whereArgs: [key]); + return rows.isNotEmpty; + } +} +'''; + +String _tplHiveProvider() => r''' +import 'package:hive_flutter/hive_flutter.dart'; +import '../app_storage.dart'; + +/// [AppStorage] backed by Hive. +/// Requires: hive_flutter: ^1.1.0 in pubspec.yaml +/// Call di().init() in main() before runApp(). +class HiveStorageProvider implements AppStorage { + late Box _box; + + @override + Future init() async { + await Hive.initFlutter(); + _box = await Hive.openBox('app_storage'); + } + + @override Future put(String key, dynamic value) async => _box.put(key, value); + @override Future get(String key) async => _box.get(key) as T?; + @override Future delete(String key) async => _box.delete(key); + @override Future clear() async => _box.clear(); + @override Future contains(String key) async => _box.containsKey(key); +} +'''; + +String _tplSharedPrefsProvider() => r''' +import 'package:shared_preferences/shared_preferences.dart'; +import '../app_storage.dart'; + +/// [AppStorage] backed by SharedPreferences. +/// Requires: shared_preferences: ^2.3.0 in pubspec.yaml +/// Call di().init() in main() before runApp(). +class SharedPrefsStorageProvider implements AppStorage { + late SharedPreferences _prefs; + + @override + Future init() async => _prefs = await SharedPreferences.getInstance(); + + @override + Future put(String key, dynamic value) async { + if (value is int) await _prefs.setInt(key, value); + else if (value is double) await _prefs.setDouble(key, value); + else if (value is bool) await _prefs.setBool(key, value); + else await _prefs.setString(key, value.toString()); + } + + @override Future get(String key) async => _prefs.get(key) as T?; + @override Future delete(String key) async => _prefs.remove(key); + @override Future clear() async => _prefs.clear(); + @override Future contains(String key) async => _prefs.containsKey(key); +} +'''; + +// ─── Data/Domain templates (shared across all state mgmt choices) ───────────── + +String _tplLocalDataSources(String module, String className, + {bool withStorage = false}) { + if (!withStorage) { + return '''import 'package:injectable/injectable.dart'; + +abstract class ${className}LocalDataSources {} + +@LazySingleton(as: ${className}LocalDataSources) +class ${className}LocalDataSourcesImpl implements ${className}LocalDataSources { + const ${className}LocalDataSourcesImpl(); +} +'''; + } + return '''import 'package:boilerplate/core/storage/app_storage.dart'; +import 'package:injectable/injectable.dart'; + +abstract class ${className}LocalDataSources { + Future cache(String key, dynamic value); + Future getCached(String key); + Future clearCache(); +} + +@LazySingleton(as: ${className}LocalDataSources) +class ${className}LocalDataSourcesImpl implements ${className}LocalDataSources { + final AppStorage _storage; + const ${className}LocalDataSourcesImpl(this._storage); + + @override + Future cache(String key, dynamic value) => _storage.put(key, value); + + @override + Future getCached(String key) => _storage.get(key); + + @override + Future clearCache() => _storage.clear(); +} +'''; +} + +String _tplMapper(String module, String className) => ''' +import '../responses/${module}_response.dart'; +import '../../../domain/model/$module.dart'; + +class ${className}Mapper { + static $className mapResponseToDomain(${className}Response response) { + return $className(id: response.id); + } +} +'''; + +String _tplResponse(String module, String className) => ''' +import 'package:freezed_annotation/freezed_annotation.dart'; + +part '${module}_response.freezed.dart'; +part '${module}_response.g.dart'; + +@freezed +abstract class ${className}Response with _\$${className}Response { + const factory ${className}Response({ + required int id, + }) = _${className}Response; + + factory ${className}Response.fromJson(Map json) => + _\$${className}ResponseFromJson(json); +} +'''; + +String _tplRemoteDataSources(String module, String className) => ''' +import 'package:boilerplate/core/client/network_service.dart'; +import 'package:injectable/injectable.dart'; + +import '../model/responses/${module}_response.dart'; + +abstract class ${className}RemoteDataSources { + Future<${className}Response> getSomething(); +} + +@LazySingleton(as: ${className}RemoteDataSources) +class ${className}RemoteDataSourceImpl implements ${className}RemoteDataSources { + final NetworkService _networkService; + const ${className}RemoteDataSourceImpl(this._networkService); + + @override + Future<${className}Response> getSomething() async { + // TODO: implement via _networkService + throw UnimplementedError(); + } +} +'''; + +String _tplRepositoryImpl(String module, String className) => ''' +import 'package:boilerplate/core/client/api_call.dart'; +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +import 'local/${module}_local_data_sources.dart'; +import 'model/mapper/${module}_mapper.dart'; +import 'remote/${module}_remote_data_sources.dart'; +import '../domain/model/$module.dart'; +import '../domain/repository/${module}_repository.dart'; + +@LazySingleton(as: ${className}Repository) +class ${className}RepositoryImpl implements ${className}Repository { + final ${className}RemoteDataSources _remote; + final ${className}LocalDataSources _local; + + const ${className}RepositoryImpl(this._remote, this._local); + + @override + Future> getSomething() { + return apiCall<$className>( + func: _remote.getSomething(), + mapper: (value) => ${className}Mapper.mapResponseToDomain(value), + ); + } +} +'''; + +String _tplModel(String className) => ''' +class $className { + final int id; + const $className({required this.id}); +} +'''; + +String _tplRepository(String module, String className) => ''' +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:dartz/dartz.dart'; + +import '../model/$module.dart'; + +abstract class ${className}Repository { + Future> getSomething(); +} +'''; + +String _tplUseCases(String module, String className) => ''' +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:dartz/dartz.dart'; + +import '../model/$module.dart'; + +abstract class ${className}UseCases { + Future> getSomething(); +} +'''; + +String _tplInteractor(String module, String className) => ''' +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +import 'model/$module.dart'; +import 'repository/${module}_repository.dart'; +import 'use_cases/${module}_use_cases.dart'; + +@LazySingleton(as: ${className}UseCases) +class ${className}Interactor implements ${className}UseCases { + final ${className}Repository _repository; + const ${className}Interactor(this._repository); + + @override + Future> getSomething() => + _repository.getSomething(); +} +'''; + +String _tplPage(String module, String className) => ''' +import 'package:flutter/material.dart'; + +class ${className}Page extends StatelessWidget { + static const route = '/$module'; + const ${className}Page({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('$className')), + body: const Center(child: Text('$className — replace me')), + ); + } +} +'''; + +// ─── Test templates ─────────────────────────────────────────────────────────── + +String _tplInteractorTest(String module, String className) => ''' +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:boilerplate/features/$module/domain/${module}_interactor.dart'; +import 'package:boilerplate/features/$module/domain/model/$module.dart'; +import 'package:boilerplate/features/$module/domain/repository/${module}_repository.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class Mock${className}Repository extends Mock implements ${className}Repository {} + +void main() { + late ${className}Interactor interactor; + late Mock${className}Repository mockRepository; + + setUp(() { + mockRepository = Mock${className}Repository(); + interactor = ${className}Interactor(mockRepository); + }); + + group('${className}Interactor', () { + test('getSomething returns data on success', () async { + when(() => mockRepository.getSomething()) + .thenAnswer((_) async => Right($className(id: 1))); + + final result = await interactor.getSomething(); + + expect(result.isRight(), true); + verify(() => mockRepository.getSomething()).called(1); + }); + + test('getSomething returns failure on error', () async { + when(() => mockRepository.getSomething()) + .thenAnswer((_) async => Left(NetworkException(message: 'error'))); + + final result = await interactor.getSomething(); + + expect(result.isLeft(), true); + }); + }); +} +'''; + +String _tplBlocTest(String module, String className) => ''' +import 'package:bloc_test/bloc_test.dart'; +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:boilerplate/features/$module/domain/model/$module.dart'; +import 'package:boilerplate/features/$module/domain/use_cases/${module}_use_cases.dart'; +import 'package:boilerplate/features/$module/presentation/blocs/${module}_bloc.dart'; +import 'package:boilerplate/features/$module/presentation/blocs/${module}_event.dart'; +import 'package:boilerplate/features/$module/presentation/blocs/${module}_state.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class Mock${className}UseCases extends Mock implements ${className}UseCases {} + +void main() { + late Mock${className}UseCases mockUseCases; + + setUp(() { + mockUseCases = Mock${className}UseCases(); + }); + + group('${className}Bloc', () { + blocTest<${className}Bloc, ${className}State>( + 'emits [Loading, Success] when getSomething succeeds', + build: () => ${className}Bloc(mockUseCases), + setUp: () { + when(() => mockUseCases.getSomething()) + .thenAnswer((_) async => Right($className(id: 1))); + }, + act: (bloc) => bloc.add(const Get${className}Event()), + expect: () => [ + const ${className}LoadingState(), + isA<${className}SuccessState>(), + ], + ); + + blocTest<${className}Bloc, ${className}State>( + 'emits [Loading, Error] when getSomething fails', + build: () => ${className}Bloc(mockUseCases), + setUp: () { + when(() => mockUseCases.getSomething()) + .thenAnswer((_) async => Left(NetworkException(message: 'error'))); + }, + act: (bloc) => bloc.add(const Get${className}Event()), + expect: () => [ + const ${className}LoadingState(), + isA<${className}ErrorState>(), + ], + ); + }); +} +'''; + +String _tplCubitTest(String module, String className) => ''' +import 'package:bloc_test/bloc_test.dart'; +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:boilerplate/features/$module/domain/model/$module.dart'; +import 'package:boilerplate/features/$module/domain/use_cases/${module}_use_cases.dart'; +import 'package:boilerplate/features/$module/presentation/blocs/${module}_cubit.dart'; +import 'package:boilerplate/features/$module/presentation/blocs/${module}_state.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class Mock${className}UseCases extends Mock implements ${className}UseCases {} + +void main() { + late Mock${className}UseCases mockUseCases; + + setUp(() { + mockUseCases = Mock${className}UseCases(); + }); + + group('${className}Cubit', () { + blocTest<${className}Cubit, ${className}State>( + 'emits [Loading, Success] when getSomething succeeds', + build: () => ${className}Cubit(mockUseCases), + setUp: () { + when(() => mockUseCases.getSomething()) + .thenAnswer((_) async => Right($className(id: 1))); + }, + act: (cubit) => cubit.getSomething(), + expect: () => [ + const ${className}LoadingState(), + isA<${className}SuccessState>(), + ], + ); + + blocTest<${className}Cubit, ${className}State>( + 'emits [Loading, Error] when getSomething fails', + build: () => ${className}Cubit(mockUseCases), + setUp: () { + when(() => mockUseCases.getSomething()) + .thenAnswer((_) async => Left(NetworkException(message: 'error'))); + }, + act: (cubit) => cubit.getSomething(), + expect: () => [ + const ${className}LoadingState(), + isA<${className}ErrorState>(), + ], + ); + }); +} +'''; + +String _tplRiverpodTest(String module, String className) => ''' +import 'package:boilerplate/core/client/network_exception.dart'; +import 'package:boilerplate/features/$module/domain/model/$module.dart'; +import 'package:boilerplate/features/$module/domain/use_cases/${module}_use_cases.dart'; +import 'package:boilerplate/features/$module/presentation/providers/${module}_notifier.dart'; +import 'package:boilerplate/features/$module/presentation/providers/${module}_state.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class Mock${className}UseCases extends Mock implements ${className}UseCases {} + +void main() { + late Mock${className}UseCases mockUseCases; + + setUp(() { + mockUseCases = Mock${className}UseCases(); + }); + + ProviderContainer makeContainer() => ProviderContainer( + overrides: [ + ${module}UseCasesProvider.overrideWithValue(mockUseCases), + ], + ); + + group('${className}Notifier', () { + test('initial state is ${className}InitialState', () async { + when(() => mockUseCases.getSomething()) + .thenAnswer((_) async => Right($className(id: 1))); + + final container = makeContainer(); + addTearDown(container.dispose); + + final state = await container.read(${module}NotifierProvider.future); + expect(state, isA<${className}InitialState>()); + }); + }); +} +'''; diff --git a/codegen/src/utils.dart b/codegen/src/utils.dart new file mode 100644 index 0000000..e103d3a --- /dev/null +++ b/codegen/src/utils.dart @@ -0,0 +1,93 @@ +// ignore_for_file: avoid_print +part of '../spl_manager.dart'; + +// ─── Validation + Notes ─────────────────────────────────────────────────────── + +void _validateStateChoice(String state) { + const valid = ['bloc', 'cubit', 'riverpod']; + if (!valid.contains(state)) { + _die('Unknown state: "$state"\nValid: ${valid.join(' | ')}'); + } +} + +void _printStateNotes(String state) { + if (state == 'riverpod') { + print(''); + print(' ⚠ Riverpod requires: flutter_riverpod in pubspec.yaml'); + print(' ⚠ Add ProviderScope at the root of your widget tree in main()'); + } +} + +void _printStorageNotes(String provider) { + switch (provider) { + case 'hive': + print('\n ⚠ Add: hive_flutter: ^1.1.0 to pubspec.yaml'); + print(' ⚠ Call di().init() in main() before runApp()'); + case 'shared_preferences': + print('\n ⚠ Add: shared_preferences: ^2.3.0 to pubspec.yaml'); + print(' ⚠ Call di().init() in main() before runApp()'); + case 'sqflite': + print('\n ⚠ Call di().init() in main() before runApp()'); + default: + break; + } +} + +// ─── build_runner ───────────────────────────────────────────────────────────── + +Future _runBuildRunner() async { + final result = await Process.run( + 'dart', + ['run', 'build_runner', 'build', '--delete-conflicting-outputs'], + runInShell: true, + ); + if (result.exitCode != 0) { + print('\n${result.stderr}'); + _die('build_runner failed (exit ${result.exitCode})'); + } + print(' build_runner: OK'); +} + +// ─── Utilities ──────────────────────────────────────────────────────────────── + +String _toPascalCase(String s) => s + .split(RegExp(r'[_\s-]+')) + .map((w) => w.isEmpty ? '' : '${w[0].toUpperCase()}${w.substring(1)}') + .join(); + +void _printHeader(String t) { + print(''); + print(' ══ $t ══'); + print(''); +} + +void _printHelp() { + print(''' +SPL Manager — Software Product Line CLI + +Variability: + Storage [XOR] one backend for the whole app + State Mgmt [OR] global default + per-feature override + +Commands: + list + add Scaffold a new feature (active) + add --with-storage Include local cache (AppStorage) + add --with-test Generate unit + state mgmt tests + add --shell-route Register as shell (bottom nav) route + add --state bloc|cubit|riverpod Override state mgmt for this feature + disable Move to catalog — code kept, DI removed + enable Restore from catalog — DI re-wired + remove [--yes|-y] Hard delete (active or catalog) + storage set Switch storage (XOR) + storage list + state set Change default state mgmt + state list + fix Re-run build_runner +'''); +} + +Never _die(String msg) { + stderr.writeln('\n ✗ $msg\n'); + exit(1); +} From b5ccdd231468ccd5199577aecfd6cd282857b894 Mon Sep 17 00:00:00 2001 From: MHibriziF Date: Sun, 15 Mar 2026 23:48:05 +0700 Subject: [PATCH 09/10] docs: feature update --- SPL.md | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/SPL.md b/SPL.md index cb3fac4..bcb6692 100644 --- a/SPL.md +++ b/SPL.md @@ -4,13 +4,15 @@ This project uses Software Product Line Engineering (SPLE) to manage variability All variability is managed through a single CLI tool and a single config file. +The CLI source lives in `codegen/spl_manager.dart` and is split across `codegen/src/` using Dart's `part`/`part of` system — see [CLI Source Layout](#cli-source-layout). + --- ## Quick Reference ``` dart run codegen/spl_manager.dart list -dart run codegen/spl_manager.dart add [--with-storage] [--state bloc|cubit|riverpod] +dart run codegen/spl_manager.dart add [--with-storage] [--with-test] [--shell-route] [--state bloc|cubit|riverpod] dart run codegen/spl_manager.dart disable # deactivate, keep code dart run codegen/spl_manager.dart enable # restore from catalog dart run codegen/spl_manager.dart remove [--yes|-y] # hard delete @@ -148,7 +150,9 @@ Features have three states: ``` dart run codegen/spl_manager.dart add -dart run codegen/spl_manager.dart add --with-storage +dart run codegen/spl_manager.dart add --with-storage # include AppStorage local cache +dart run codegen/spl_manager.dart add --with-test # generate unit + state mgmt tests +dart run codegen/spl_manager.dart add --shell-route # register as shell (bottom nav) route dart run codegen/spl_manager.dart add --state cubit dart run codegen/spl_manager.dart add --with-storage --state riverpod ``` @@ -178,9 +182,13 @@ lib/features// providers/ (riverpod only) _state.dart _notifier.dart + +test/features// (only with --with-test) + domain/_interactor_test.dart + presentation/_bloc_test.dart | _cubit_test.dart | _notifier_test.dart ``` -After scaffolding, register the route manually in `lib/core/router/app_router_config.dart`. +The route is injected automatically into `lib/core/router/app_router_config.dart`. Use `--shell-route` to register it inside the `ShellRoute` (bottom nav); omit it for a top-level route. DI is auto-wired — `build_runner` regenerates `lib/services/di.config.dart` automatically. @@ -244,6 +252,26 @@ Brick templates live in `bricks/`. They are excluded from Dart analysis (`analys --- +## CLI Source Layout + +The CLI is split across multiple files using Dart's `part`/`part of` directives. All parts share a single library — `codegen/spl_manager.dart` — so every private function is accessible everywhere with no extra imports. + +``` +codegen/ +├── spl_manager.dart entry point — library declaration, import 'dart:io', main(), part directives +└── src/ + ├── commands.dart _cmdList, _cmdAdd, _cmdDisable, _cmdEnable, _cmdRemove, _cmdStorage*, _cmdState*, _cmdFix + ├── generators.dart _generateFeatureFiles, _stateFiles, _injectRoute, _removeRoute, _removeTests, _generateTestFiles + ├── storage_manager.dart _getActiveProviderName, _implFileName, _deleteStorageImpl, _generateStorageImpl, _rewriteStorageModule + ├── templates.dart all _tpl* functions — state mgmt, storage providers, data/domain layer, tests + ├── spl_config.dart spl.yaml read/write helpers, Mason integration (_checkMason, _tryMason*) + └── utils.dart _validateStateChoice, _printNotes, _runBuildRunner, _toPascalCase, _printHelp, _die +``` + +To extend the CLI — add a command, add a template — edit only the relevant part file. + +--- + ## DI Regeneration All generated code uses `@injectable` / `@lazySingleton` annotations. After any `add` or `remove` command, `build_runner` is run automatically to regenerate `lib/services/di.config.dart`. From e0281a08842bd3eda5568e4c2a6a8196789bf0ca Mon Sep 17 00:00:00 2001 From: MHibriziF Date: Mon, 16 Mar 2026 06:07:44 +0700 Subject: [PATCH 10/10] refactor: check for cross dependencies, allow for multiple feature in one command --- SPL.md | 133 ++++++++++----- codegen/spl_manager.dart | 131 +++++++++++---- codegen/src/commands.dart | 234 +++++++++++++++++++++------ codegen/src/generators.dart | 29 +++- codegen/src/spl_config.dart | 33 +++- codegen/src/storage_manager.dart | 114 +++++++++---- codegen/src/templates.dart | 8 +- codegen/src/utils.dart | 97 +++++++++-- lib/core/storage/storage_module.dart | 9 +- lib/services/di.config.dart | 5 +- spl.yaml | 19 ++- 11 files changed, 629 insertions(+), 183 deletions(-) diff --git a/SPL.md b/SPL.md index bcb6692..c9a9d3b 100644 --- a/SPL.md +++ b/SPL.md @@ -12,11 +12,13 @@ The CLI source lives in `codegen/spl_manager.dart` and is split across `codegen/ ``` dart run codegen/spl_manager.dart list -dart run codegen/spl_manager.dart add [--with-storage] [--with-test] [--shell-route] [--state bloc|cubit|riverpod] -dart run codegen/spl_manager.dart disable # deactivate, keep code -dart run codegen/spl_manager.dart enable # restore from catalog -dart run codegen/spl_manager.dart remove [--yes|-y] # hard delete -dart run codegen/spl_manager.dart storage set +dart run codegen/spl_manager.dart add [spec2 ...] +dart run codegen/spl_manager.dart disable [name2 ...] +dart run codegen/spl_manager.dart enable [name2 ...] +dart run codegen/spl_manager.dart remove [name2 ...] [--yes|-y] +dart run codegen/spl_manager.dart storage add +dart run codegen/spl_manager.dart storage remove +dart run codegen/spl_manager.dart storage default dart run codegen/spl_manager.dart storage list dart run codegen/spl_manager.dart state set dart run codegen/spl_manager.dart state list @@ -27,7 +29,7 @@ dart run codegen/spl_manager.dart fix ## Source of Truth: `spl.yaml` -`spl.yaml` is the single source of truth for the product configuration. It tracks the active storage backend, the default state management solution, and all features. +`spl.yaml` is the single source of truth for the product configuration. It tracks the active storage backends, the default state management solution, and all features. Do not edit `spl.yaml` by hand — use the CLI. The CLI updates this file, generates/deletes code, and re-wires DI automatically. @@ -35,30 +37,33 @@ Do not edit `spl.yaml` by hand — use the CLI. The CLI updates this file, gener ## Variability Points -This project has two variability points. They have different exclusivity rules. +This project has two variability points. Both use OR semantics. -### 1. Storage — XOR (exactly one active) +### 1. Storage — OR (multiple backends can coexist) -Controls the backend for `AppStorage`, the general-purpose local caching interface used by features that need to persist data locally (e.g., cached lists, user preferences). +Controls the backend(s) for `AppStorage`, the general-purpose local caching interface. Multiple providers can be active simultaneously — each feature declares which one it uses via `@Named`. | Provider | Notes | |---|---| | `flutter_secure_storage` | Default. Encrypted key-value. No `init()` needed. | | `hive` | Fast binary key-value. Requires `hive_flutter` in `pubspec.yaml` and `AppStorage.init()` before `runApp()`. | -| `sqflite` | SQLite. Requires `AppStorage.init()` before `runApp()`. | +| `sqflite` | SQLite (relational). Best for structured/queryable data. Requires `AppStorage.init()` before `runApp()`. | | `shared_preferences` | Simple unencrypted key-value. Requires `shared_preferences` in `pubspec.yaml` and `AppStorage.init()` before `runApp()`. | -**XOR means**: switching providers deletes the old implementation file and generates the new one. Only the active provider's impl file exists in `lib/core/storage/impl/`. +Each active provider is registered as `@Named('provider_name')` in `StorageModule`. Features inject the named variant they need. ``` -dart run codegen/spl_manager.dart storage set hive +dart run codegen/spl_manager.dart storage add sqflite +dart run codegen/spl_manager.dart storage remove hive +dart run codegen/spl_manager.dart storage default sqflite # default for --with-storage +dart run codegen/spl_manager.dart storage list ``` -This regenerates `lib/core/storage/impl/hive_storage_provider.dart`, rewrites `lib/core/storage/storage_module.dart` to wire the new impl, and runs `build_runner`. +When a feature is added with `--storage sqflite` (or `,storage=sqflite` inline), the CLI auto-registers `sqflite` if not already active, generates the impl file, and wires `@Named('sqflite')` into the feature's local data source. ### 2. State Management — OR (global default + per-feature override) -Controls the presentation layer pattern for features. Unlike storage, this is **not exclusive** — different features in the same app can use different state management solutions. +Controls the presentation layer pattern for features. Different features in the same app can use different solutions. | Solution | Package | Files generated | Use when | |---|---|---|---| @@ -74,6 +79,7 @@ dart run codegen/spl_manager.dart state set cubit Override per feature at creation time: ``` dart run codegen/spl_manager.dart add orders --state riverpod +dart run codegen/spl_manager.dart add orders,state=riverpod # inline equivalent ``` Bloc and cubit coexist with zero config (same `flutter_bloc` package). Riverpod requires: @@ -108,24 +114,26 @@ The alternative backends (Hive, SQLite, SharedPreferences) write plaintext or we ``` lib/core/storage/app_storage.dart -lib/core/storage/impl/.dart ← only one file exists at a time -lib/core/storage/storage_module.dart ← SPL-managed, do not edit manually +lib/core/storage/impl/.dart ← one file per active provider +lib/core/storage/storage_module.dart ← SPL-managed, do not edit manually ``` -Used by features that need to cache data locally — product lists, user preferences, onboarding state, etc. The backend is switchable via `storage set`. No security guarantee is assumed. +Used by features that need to cache data locally — product lists, user preferences, onboarding state, etc. Multiple backends can be active at once; each feature picks its own. No security guarantee is assumed. -Inject it in your local data source: +Generated local data sources use `@Named` to inject the correct backend: ```dart -@LazySingleton(as: MyLocalDataSources) -class MyLocalDataSourcesImpl implements MyLocalDataSources { +@LazySingleton(as: OrdersLocalDataSources) +class OrdersLocalDataSourcesImpl implements OrdersLocalDataSources { final AppStorage _storage; - MyLocalDataSourcesImpl(this._storage); + const OrdersLocalDataSourcesImpl(@Named('sqflite') this._storage); } ``` Add a feature with local storage pre-wired: ``` -dart run codegen/spl_manager.dart add orders --with-storage +dart run codegen/spl_manager.dart add orders --with-storage # uses default backend +dart run codegen/spl_manager.dart add orders --storage sqflite # specific backend +dart run codegen/spl_manager.dart add orders,storage=sqflite # inline equivalent ``` --- @@ -148,21 +156,56 @@ Features have three states: ### Adding a feature +Each argument to `add` is a **feature spec**: a feature name optionally followed by comma-separated inline options. + +``` +[,storage=][,state=][,test][,shell] +``` + +Examples: ``` -dart run codegen/spl_manager.dart add -dart run codegen/spl_manager.dart add --with-storage # include AppStorage local cache -dart run codegen/spl_manager.dart add --with-test # generate unit + state mgmt tests -dart run codegen/spl_manager.dart add --shell-route # register as shell (bottom nav) route -dart run codegen/spl_manager.dart add --state cubit -dart run codegen/spl_manager.dart add --with-storage --state riverpod +# Single feature, no options +dart run codegen/spl_manager.dart add orders + +# Single feature with options inline +dart run codegen/spl_manager.dart add orders,storage=sqflite,state=cubit,test + +# Multiple features, each with their own config +dart run codegen/spl_manager.dart add orders,storage=sqflite,state=cubit feed,test settings,shell + +# Global flags apply to all features that don't override them inline +dart run codegen/spl_manager.dart add orders inventory,storage=hive settings --state bloc --with-test +# → orders: bloc + test (from global flags) +# → inventory: hive + test (storage from inline, test from global) +# → settings: bloc + test (from global flags) ``` +**Available inline keys:** + +| Key | Equivalent flag | Description | +|---|---|---| +| `storage=` | `--storage ` | Use a specific backend | +| `with-storage` or `ws` | `--with-storage` | Use the default backend | +| `state=` | `--state ` | State management override | +| `test` | `--with-test` | Generate tests | +| `shell` | `--shell-route` | Register as shell (bottom nav) route | + +**Global flags** (apply to all features unless overridden inline): + +| Flag | Description | +|---|---| +| `--with-storage` | Use default storage backend for all | +| `--storage ` | Use specific backend for all | +| `--state ` | State management for all | +| `--with-test` | Generate tests for all | +| `--shell-route` | Shell route for all | + This scaffolds a full clean architecture feature: ``` lib/features// data/ - local/_local_data_sources.dart (only with --with-storage) + local/_local_data_sources.dart (only with storage option) model/ mapper/_mapper.dart responses/_response.dart @@ -183,27 +226,31 @@ lib/features// _state.dart _notifier.dart -test/features// (only with --with-test) +test/features// (only with test option) domain/_interactor_test.dart presentation/_bloc_test.dart | _cubit_test.dart | _notifier_test.dart ``` -The route is injected automatically into `lib/core/router/app_router_config.dart`. Use `--shell-route` to register it inside the `ShellRoute` (bottom nav); omit it for a top-level route. +The route is injected automatically into `lib/core/router/app_router_config.dart`. Use `shell` (inline) or `--shell-route` (global) to register inside the `ShellRoute` (bottom nav); omit for a top-level route. -DI is auto-wired — `build_runner` regenerates `lib/services/di.config.dart` automatically. +DI is auto-wired — `build_runner` regenerates `lib/services/di.config.dart` automatically. When adding multiple features, `build_runner` runs once at the end. ### Disabling a feature ``` dart run codegen/spl_manager.dart disable +dart run codegen/spl_manager.dart disable orders inventory settings # multiple at once ``` Moves `lib/features//` to `features_catalog//`, marks it `inactive` in `spl.yaml`, and regenerates DI. The code is fully preserved — nothing is deleted. +If other features import the disabled feature, a warning is printed listing the affected files. The disable proceeds — fix the broken imports afterwards. + ### Re-enabling a feature ``` dart run codegen/spl_manager.dart enable +dart run codegen/spl_manager.dart enable orders inventory # multiple at once ``` Moves `features_catalog//` back to `lib/features//`, marks it `active` in `spl.yaml`, and re-wires DI. All original code is restored exactly as it was left. @@ -212,10 +259,13 @@ Moves `features_catalog//` back to `lib/features//`, marks it `activ ``` dart run codegen/spl_manager.dart remove -dart run codegen/spl_manager.dart remove --yes +dart run codegen/spl_manager.dart remove --yes # skip confirmation +dart run codegen/spl_manager.dart remove orders inventory --yes # multiple at once ``` -Permanently deletes the feature from wherever it lives (active or catalog) and removes it from `spl.yaml`. Irreversible. Add `--yes` (or `-y`) to skip the confirmation prompt. +Permanently deletes the feature from wherever it lives (active or catalog), removes its route and tests, and removes it from `spl.yaml`. Irreversible. + +If other features import the feature being removed, the CLI **blocks** and lists the dependent files. Pass `--yes` to force the deletion anyway (you will need to fix the broken imports manually). --- @@ -226,10 +276,7 @@ The four features included in this template (`authentication`, `onboarding`, `pr Keep them as reference — remove them when you no longer need the examples: ``` -dart run codegen/spl_manager.dart remove authentication --yes -dart run codegen/spl_manager.dart remove onboarding --yes -dart run codegen/spl_manager.dart remove product --yes -dart run codegen/spl_manager.dart remove profile --yes +dart run codegen/spl_manager.dart remove authentication onboarding product profile --yes ``` --- @@ -261,11 +308,11 @@ codegen/ ├── spl_manager.dart entry point — library declaration, import 'dart:io', main(), part directives └── src/ ├── commands.dart _cmdList, _cmdAdd, _cmdDisable, _cmdEnable, _cmdRemove, _cmdStorage*, _cmdState*, _cmdFix - ├── generators.dart _generateFeatureFiles, _stateFiles, _injectRoute, _removeRoute, _removeTests, _generateTestFiles - ├── storage_manager.dart _getActiveProviderName, _implFileName, _deleteStorageImpl, _generateStorageImpl, _rewriteStorageModule + ├── generators.dart _generateFeatureFiles, _stateFiles, _injectRoute, _removeRoute, _removeTests, _generateTestFiles, _checkCrossFeatureDeps + ├── storage_manager.dart _getDefaultProviderName, _getActiveProviders, _ensureStorageActive, _rewriteStorageModule, _implFileName, _deleteStorageImpl, _generateStorageImpl ├── templates.dart all _tpl* functions — state mgmt, storage providers, data/domain layer, tests ├── spl_config.dart spl.yaml read/write helpers, Mason integration (_checkMason, _tryMason*) - └── utils.dart _validateStateChoice, _printNotes, _runBuildRunner, _toPascalCase, _printHelp, _die + └── utils.dart _parseFeatureSpec, _validateStateChoice, _printNotes, _runBuildRunner, _toPascalCase, _printHelp, _die ``` To extend the CLI — add a command, add a template — edit only the relevant part file. @@ -274,7 +321,9 @@ To extend the CLI — add a command, add a template — edit only the relevant p ## DI Regeneration -All generated code uses `@injectable` / `@lazySingleton` annotations. After any `add` or `remove` command, `build_runner` is run automatically to regenerate `lib/services/di.config.dart`. +All generated code uses `@injectable` / `@lazySingleton` annotations. After any `add`, `disable`, `enable`, or `remove` command, `build_runner` is run automatically to regenerate `lib/services/di.config.dart`. + +When operating on multiple features at once, `build_runner` runs **once** at the end rather than after each feature. To run it manually: ``` diff --git a/codegen/spl_manager.dart b/codegen/spl_manager.dart index 2786dac..4072996 100644 --- a/codegen/spl_manager.dart +++ b/codegen/spl_manager.dart @@ -2,7 +2,7 @@ /// SPL Manager — Software Product Line CLI for Flutter Clean Architecture /// /// Variability points: -/// Storage (XOR) — one backend for the whole app +/// Storage (OR) — one or more backends, each registered with @Named /// State Mgmt (OR) — global default, per-feature override allowed /// /// Usage: @@ -10,11 +10,13 @@ /// /// Commands: /// list -/// add [--with-storage] [--with-test] [--shell-route] [--state bloc|cubit|riverpod] -/// disable Move feature to catalog (keeps code, unwires DI) -/// enable Restore feature from catalog (wires DI) -/// remove [--yes|-y] Hard delete (works on active or catalog features) -/// storage set flutter_secure_storage|sqflite|hive|shared_preferences +/// add [name2 ...] [--with-storage] [--storage ] [--with-test] [--shell-route] [--state bloc|cubit|riverpod] +/// disable [name2 ...] Move feature(s) to catalog (keeps code, unwires DI) +/// enable [name2 ...] Restore feature(s) from catalog (wires DI) +/// remove [name2 ...] [--yes|-y] Hard delete +/// storage add +/// storage remove +/// storage default /// storage list /// state set bloc|cubit|riverpod /// state list @@ -38,40 +40,101 @@ void main(List args) async { switch (args[0]) { case 'list': await _cmdList(); + case 'add': - if (args.length < 2) _die('Usage: add [--with-storage] [--with-test] [--shell-route] [--state bloc|cubit|riverpod]'); - final withStorage = args.contains('--with-storage'); - final withTest = args.contains('--with-test'); - final shellRoute = args.contains('--shell-route'); - final stateIdx = args.indexOf('--state'); - final stateOverride = stateIdx != -1 && stateIdx + 1 < args.length - ? args[stateIdx + 1] - : null; - await _cmdAdd(args[1], - withStorage: withStorage, - withTest: withTest, - shellRoute: shellRoute, - stateOverride: stateOverride); + // Global defaults from flags + final stateIdx = args.indexOf('--state'); + final globalState = stateIdx != -1 && stateIdx + 1 < args.length ? args[stateIdx + 1] : null; + final storageIdx = args.indexOf('--storage'); + final globalStorageOverride = storageIdx != -1 && storageIdx + 1 < args.length ? args[storageIdx + 1] : null; + final globalWithStorage = args.contains('--with-storage') || globalStorageOverride != null; + final globalWithTest = args.contains('--with-test'); + final globalShellRoute = args.contains('--shell-route'); + // Collect feature specs — positional args (may include inline ,options) + final specs = []; + for (var i = 1; i < args.length; i++) { + if (args[i].startsWith('-')) continue; + if (stateIdx != -1 && i == stateIdx + 1) continue; + if (storageIdx != -1 && i == storageIdx + 1) continue; + specs.add(args[i]); + } + if (specs.isEmpty) _die( + 'Usage: add [,storage=

][,state=][,test][,shell] [name2[,...]] ...\n' + ' Global flags (apply to all unless overridden inline):\n' + ' --with-storage --storage --with-test --shell-route --state ', + ); + for (final spec in specs) { + final f = _parseFeatureSpec(spec, + globalStorageOverride: globalStorageOverride, + globalWithStorage: globalWithStorage, + globalState: globalState, + globalWithTest: globalWithTest, + globalShellRoute: globalShellRoute); + await _cmdAdd(f.name, + withStorage: f.withStorage, + storageOverride: f.storageOverride, + withTest: f.withTest, + shellRoute: f.shellRoute, + stateOverride: f.stateOverride, + runDi: false); + } + print('\n Wiring DI (build_runner)...'); + await _runBuildRunner(); + if (specs.length == 1) { + final module = specs[0].split(',')[0].toLowerCase().replaceAll(RegExp(r'[^a-z0-9_]'), '_'); + print('\n ✓ Done! lib/features/$module/'); + } else { + print('\n ✓ Done! ${specs.length} features added.'); + } + case 'disable': - if (args.length < 2) _die('Usage: disable '); - await _cmdDisable(args[1]); + final names = args.skip(1).where((a) => !a.startsWith('-')).toList(); + if (names.isEmpty) _die('Usage: disable [name2 ...]'); + for (final name in names) await _cmdDisable(name, runDi: false); + print(' Regenerating DI...'); + await _runBuildRunner(); + if (names.length > 1) print('\n ✓ ${names.length} features disabled.'); + case 'enable': - if (args.length < 2) _die('Usage: enable '); - await _cmdEnable(args[1]); + final names = args.skip(1).where((a) => !a.startsWith('-')).toList(); + if (names.isEmpty) _die('Usage: enable [name2 ...]'); + for (final name in names) await _cmdEnable(name, runDi: false); + print(' Wiring DI (build_runner)...'); + await _runBuildRunner(); + if (names.length > 1) print('\n ✓ ${names.length} features enabled.'); + case 'remove': - if (args.length < 2) _die('Usage: remove [--yes|-y]'); + final names = args.skip(1).where((a) => !a.startsWith('-')).toList(); + if (names.isEmpty) _die('Usage: remove [name2 ...] [--yes|-y]'); final force = args.contains('--yes') || args.contains('-y'); - await _cmdRemove(args[1], force: force); + var needsDi = false; + for (final name in names) { + if (await _cmdRemove(name, force: force, runDi: false)) needsDi = true; + } + if (needsDi) { + print(' Regenerating DI...'); + await _runBuildRunner(); + } + if (names.length > 1) print('\n ✓ ${names.length} features removed.'); + case 'storage': - if (args.length < 2) _die('Usage: storage set | storage list'); - if (args[1] == 'set') { - if (args.length < 3) _die('Usage: storage set '); - await _cmdStorageSet(args[2]); - } else if (args[1] == 'list') { - _cmdStorageList(); - } else { - _die('Unknown storage subcommand: ${args[1]}'); + if (args.length < 2) _die('Usage: storage add|remove|default|list []'); + switch (args[1]) { + case 'add': + if (args.length < 3) _die('Usage: storage add '); + await _cmdStorageAdd(args[2]); + case 'remove': + if (args.length < 3) _die('Usage: storage remove '); + await _cmdStorageRemove(args[2]); + case 'default': + if (args.length < 3) _die('Usage: storage default '); + _cmdStorageDefault(args[2]); + case 'list': + _cmdStorageList(); + default: + _die('Unknown storage subcommand: ${args[1]}\nValid: add | remove | default | list'); } + case 'state': if (args.length < 2) _die('Usage: state set | state list'); if (args[1] == 'set') { @@ -82,8 +145,10 @@ void main(List args) async { } else { _die('Unknown state subcommand: ${args[1]}'); } + case 'fix': await _cmdFix(); + default: _die('Unknown command: ${args[0]}'); } diff --git a/codegen/src/commands.dart b/codegen/src/commands.dart index f49c1f0..7239850 100644 --- a/codegen/src/commands.dart +++ b/codegen/src/commands.dart @@ -7,11 +7,12 @@ Future _cmdList() async { final config = _readSplConfig(); _printHeader('SPL Configuration'); - final storage = config['storage']?['local_backend'] ?? 'flutter_secure_storage'; - final stateDefault = config['state_management']?['default'] ?? 'bloc'; + final storageDefault = _getDefaultProviderName(); + final storageActive = _getActiveProviders(); + final stateDefault = config['state_management']?['default'] ?? 'bloc'; print(' App : ${config['app']?['name'] ?? 'unknown'}'); - print(' Storage [XOR] : $storage'); + print(' Storage [OR] : ${storageActive.join(', ')} (default: $storageDefault)'); print(' State Mgmt [OR] : $stateDefault (default, per-feature override allowed)'); print(''); @@ -62,9 +63,11 @@ Future _cmdList() async { Future _cmdAdd( String name, { bool withStorage = false, + String? storageOverride, bool withTest = false, bool shellRoute = false, String? stateOverride, + bool runDi = true, }) async { final module = name.toLowerCase().replaceAll(RegExp(r'[^a-z0-9_]'), '_'); final className = _toPascalCase(module); @@ -80,15 +83,27 @@ Future _cmdAdd( } final config = _readSplConfig(); - final globalStorageBackend = config['storage']?['local_backend'] ?? 'flutter_secure_storage'; final globalStateDefault = config['state_management']?['default'] ?? 'bloc'; final stateChoice = stateOverride ?? globalStateDefault; - _validateStateChoice(stateChoice); + // Resolve effective storage provider + String? storageProvider; + if (storageOverride != null) { + _validateStorageProvider(storageOverride); + storageProvider = storageOverride; + } else if (withStorage) { + storageProvider = _getDefaultProviderName(); + } + + // Auto-register provider if not already active + if (storageProvider != null) { + await _ensureStorageActive(storageProvider); + } + _printHeader('Adding feature: $module'); print(' Class : $className'); - print(' Storage : ${withStorage ? globalStorageBackend : 'none'}'); + print(' Storage : ${storageProvider ?? 'none'}'); print(' State : $stateChoice${stateOverride != null ? ' (override)' : ' (default)'}'); print(' Route : ${shellRoute ? 'shell (bottom nav)' : 'top-level'}'); print(' Tests : ${withTest ? 'yes (--with-test)' : 'no'}'); @@ -96,15 +111,15 @@ Future _cmdAdd( print(''); final usedMason = await _tryMasonFeature(module, - withStorage: withStorage, state: stateChoice); + withStorage: storageProvider != null, state: stateChoice); if (!usedMason) { _generateFeatureFiles(module, className, - withStorage: withStorage, state: stateChoice); + storageProvider: storageProvider, state: stateChoice); } _addFeatureToConfig( module, - storage: withStorage ? globalStorageBackend : 'none', + storage: storageProvider ?? 'none', state: stateChoice, ); @@ -114,16 +129,17 @@ Future _cmdAdd( _printStateNotes(stateChoice); - print('\n Wiring DI (build_runner)...'); - await _runBuildRunner(); - - print('\n ✓ Done! lib/features/$module/'); + if (runDi) { + print('\n Wiring DI (build_runner)...'); + await _runBuildRunner(); + print('\n ✓ Done! lib/features/$module/'); + } } -Future _cmdDisable(String name) async { +Future _cmdDisable(String name, {bool runDi = true}) async { final module = name.toLowerCase(); - final activeDir = 'lib/features/$module'; - final catalogDir = 'features_catalog/$module'; + final activeDir = 'lib/features/$module'; + final catalogDir = 'features_catalog/$module'; _printHeader('Disabling feature: $module'); @@ -134,6 +150,18 @@ Future _cmdDisable(String name) async { _die('Feature "$module" not found.'); } + // Cross-feature dependency check — warn only, don't block + final deps = _checkCrossFeatureDeps(module); + if (deps.isNotEmpty) { + print(' ⚠ Other features reference "$module":'); + for (final entry in deps.entries) { + print(' ${entry.key}'); + for (final line in entry.value) print(' $line'); + } + print(' These references will break once "$module" is disabled. Fix them after.'); + print(''); + } + Directory('features_catalog').createSync(); Directory(activeDir).renameSync(catalogDir); print(' ○ Moved: $activeDir → $catalogDir'); @@ -141,16 +169,18 @@ Future _cmdDisable(String name) async { _removeRoute(module); _updateFeatureStatusInConfig(module, 'inactive'); - print(' Regenerating DI...'); - await _runBuildRunner(); + if (runDi) { + print(' Regenerating DI...'); + await _runBuildRunner(); + } print('\n ✓ Feature "$module" disabled.'); print(' → Restore with: dart run codegen/spl_manager.dart enable $module'); } -Future _cmdEnable(String name) async { +Future _cmdEnable(String name, {bool runDi = true}) async { final module = name.toLowerCase(); - final activeDir = 'lib/features/$module'; - final catalogDir = 'features_catalog/$module'; + final activeDir = 'lib/features/$module'; + final catalogDir = 'features_catalog/$module'; _printHeader('Enabling feature: $module'); @@ -168,13 +198,19 @@ Future _cmdEnable(String name) async { _updateFeatureStatusInConfig(module, 'active'); - print(' Wiring DI (build_runner)...'); - await _runBuildRunner(); - print('\n ✓ Feature "$module" enabled.'); - print(' → Ensure route is registered in lib/core/router/app_router_config.dart'); + if (runDi) { + print(' Wiring DI (build_runner)...'); + await _runBuildRunner(); + print('\n ✓ Feature "$module" enabled.'); + print(' → Ensure route is registered in lib/core/router/app_router_config.dart'); + } else { + print(' ✓ Feature "$module" enabled.'); + print(' → Ensure route is registered in lib/core/router/app_router_config.dart'); + } } -Future _cmdRemove(String name, {bool force = false}) async { +/// Returns true if the feature was active (DI regeneration needed). +Future _cmdRemove(String name, {bool force = false, bool runDi = true}) async { final module = name.toLowerCase(); final activeDir = 'lib/features/$module'; final catalogDir = 'features_catalog/$module'; @@ -188,6 +224,23 @@ Future _cmdRemove(String name, {bool force = false}) async { _printHeader('Removing feature: $module'); print(' Location: $location${inCatalog ? ' (disabled)' : ' (active)'}'); + // Cross-feature dependency check — block if deps found and not forced + final deps = _checkCrossFeatureDeps(module); + if (deps.isNotEmpty) { + print(' ⚠ Other features reference "$module":'); + for (final entry in deps.entries) { + print(' ${entry.key}'); + for (final line in entry.value) print(' $line'); + } + print(''); + if (!force) { + _die('Cannot remove "$module" — other features depend on it.\n' + ' Fix the references first, or use --yes to force the deletion anyway.'); + } + print(' Forcing removal despite cross-feature references.'); + print(''); + } + if (!force) { stdout.write(' Permanently delete "$module"? [y/N] '); final confirm = stdin.readLineSync()?.toLowerCase(); @@ -200,57 +253,142 @@ Future _cmdRemove(String name, {bool force = false}) async { _removeTests(module); _removeFeatureFromConfig(module); - if (inActive) { + if (inActive && runDi) { print(' Regenerating DI...'); await _runBuildRunner(); } print('\n ✓ Feature "$module" permanently removed.'); + + return inActive; } -Future _cmdStorageSet(String provider) async { - const valid = ['flutter_secure_storage', 'sqflite', 'hive', 'shared_preferences']; - if (!valid.contains(provider)) { - _die('Unknown provider: "$provider"\nValid: ${valid.join(' | ')}'); - } +// ─── Storage commands ───────────────────────────────────────────────────────── - final current = _getActiveProviderName(); - if (current == provider) { print('\n Already using "$provider".'); exit(0); } +Future _cmdStorageAdd(String provider) async { + _validateStorageProvider(provider); - _printHeader('Switching storage [XOR]: $current → $provider'); + final activeProviders = _getActiveProviders(); + if (activeProviders.contains(provider)) { + print('\n Storage provider "$provider" is already active.'); + exit(0); + } - _deleteStorageImpl(current); + _printHeader('Adding storage provider: $provider'); final usedMason = await _tryMasonStorage(provider); if (!usedMason) _generateStorageImpl(provider); - _rewriteStorageModule(provider); - _updateStorageInConfig(provider); + final newProviders = [...activeProviders, provider]; + _addStorageToConfig(provider); + _rewriteStorageModule(newProviders, _getDefaultProviderName()); print('\n Regenerating DI...'); await _runBuildRunner(); - - print('\n ✓ Storage → "$provider"'); + print('\n ✓ Storage provider "$provider" added.'); _printStorageNotes(provider); } +Future _cmdStorageRemove(String provider) async { + _validateStorageProvider(provider); + + final activeProviders = _getActiveProviders(); + if (!activeProviders.contains(provider)) { + _die('Provider "$provider" is not active.'); + } + if (activeProviders.length == 1) { + _die('Cannot remove the only active storage provider.'); + } + + // Check if any active features use this provider + final config = _readSplConfig(); + final features = config['features'] as List>? ?? []; + final dependents = features + .where((f) => f['storage'] == provider && (f['status'] ?? 'active') == 'active') + .toList(); + + if (dependents.isNotEmpty) { + print(''); + print(' ⚠ Active features use "$provider":'); + for (final f in dependents) print(' ${f['name']}'); + _die('\n Cannot remove "$provider" — active features depend on it.\n' + ' Migrate those features to another backend first.'); + } + + _printHeader('Removing storage provider: $provider'); + + _deleteStorageImpl(provider); + _removeStorageFromConfig(provider); + + final newProviders = activeProviders.where((p) => p != provider).toList(); + var newDefault = _getDefaultProviderName(); + if (newDefault == provider) { + newDefault = newProviders.first; + _updateStorageDefaultInConfig(newDefault); + print(' ⚠ Default storage changed to "$newDefault"'); + } + + _rewriteStorageModule(newProviders, newDefault); + + print('\n Regenerating DI...'); + await _runBuildRunner(); + print('\n ✓ Storage provider "$provider" removed.'); +} + +void _cmdStorageDefault(String provider) { + _validateStorageProvider(provider); + + final activeProviders = _getActiveProviders(); + if (!activeProviders.contains(provider)) { + _die('Provider "$provider" is not active.\n' + ' Add it first: dart run codegen/spl_manager.dart storage add $provider'); + } + + _updateStorageDefaultInConfig(provider); + _printHeader('Storage default → $provider'); + print(' New features using --with-storage will use: $provider'); + print(' Existing features are unchanged.'); +} + void _cmdStorageList() { - _printHeader('Storage Providers [XOR — exactly one active]'); - final current = _getActiveProviderName(); - final providers = { + _printHeader('Storage Providers [OR — multiple can be active simultaneously]'); + final activeProviders = _getActiveProviders(); + final defaultProvider = _getDefaultProviderName(); + + final config = _readSplConfig(); + final features = config['features'] as List>? ?? []; + + final descriptions = { 'flutter_secure_storage': 'Encrypted key-value. Strings only. Best for sensitive data.', 'sqflite': 'SQLite (relational). Best for structured/queryable data.', 'hive': 'NoSQL box store. Fast reads. Best for object graphs.', 'shared_preferences': 'Simple key-value. Non-encrypted. Best for user settings.', }; - for (final e in providers.entries) { - final active = e.key == current ? ' ◀ active' : ''; - print(' ${e.key}$active'); - print(' ${e.value}'); + + for (final entry in descriptions.entries) { + final p = entry.key; + final isActive = activeProviders.contains(p); + final isDefault = p == defaultProvider; + final tags = [ + if (isActive) 'active', + if (isDefault) 'default', + ]; + final tagStr = tags.isEmpty ? '' : ' ◀ ${tags.join(', ')}'; + print(' $p$tagStr'); + print(' ${entry.value}'); + if (isActive) { + final users = features.where((f) => f['storage'] == p).map((f) => f['name']).toList(); + if (users.isNotEmpty) print(' Used by: ${users.join(', ')}'); + } print(''); } - print(' Switch (XOR): dart run codegen/spl_manager.dart storage set '); + + print(' dart run codegen/spl_manager.dart storage add '); + print(' dart run codegen/spl_manager.dart storage remove '); + print(' dart run codegen/spl_manager.dart storage default '); } +// ─── State commands ─────────────────────────────────────────────────────────── + void _cmdStateSet(String solution) { _validateStateChoice(solution); _updateStateDefaultInConfig(solution); diff --git a/codegen/src/generators.dart b/codegen/src/generators.dart index 45dc91a..4b8eca4 100644 --- a/codegen/src/generators.dart +++ b/codegen/src/generators.dart @@ -6,7 +6,7 @@ part of '../spl_manager.dart'; void _generateFeatureFiles( String module, String className, { - bool withStorage = false, + String? storageProvider, String state = 'bloc', }) { final dirs = [ @@ -29,7 +29,7 @@ void _generateFeatureFiles( final files = { // Data layer 'lib/features/$module/data/local/${module}_local_data_sources.dart': - _tplLocalDataSources(module, className, withStorage: withStorage), + _tplLocalDataSources(module, className, storageProvider: storageProvider), 'lib/features/$module/data/model/mapper/${module}_mapper.dart': _tplMapper(module, className), 'lib/features/$module/data/model/responses/${module}_response.dart': @@ -88,6 +88,31 @@ Map _stateFiles(String module, String className, String state) { } } +// ─── Cross-feature dependency check ────────────────────────────────────────── + +/// Scans active features and tests for any imports/references to [module]. +/// Returns a map of { filePath → [matching lines] } for all referencing files. +Map> _checkCrossFeatureDeps(String module) { + final pattern = 'features/$module/'; + final results = >{}; + + for (final root in ['lib/features', 'test/features']) { + final dir = Directory(root); + if (!dir.existsSync()) continue; + for (final entity in dir.listSync(recursive: true)) { + if (entity is! File || !entity.path.endsWith('.dart')) continue; + final normalized = entity.path.replaceAll('\\', '/'); + // Skip the module's own files + if (normalized.contains('/$module/')) continue; + final lines = entity.readAsLinesSync(); + final matches = lines.where((l) => l.contains(pattern)).map((l) => l.trim()).toList(); + if (matches.isNotEmpty) results[normalized] = matches; + } + } + + return results; +} + // ─── Route injection ────────────────────────────────────────────────────────── void _injectRoute(String module, String className, {bool shellRoute = false}) { diff --git a/codegen/src/spl_config.dart b/codegen/src/spl_config.dart index 68b54e1..d4982bb 100644 --- a/codegen/src/spl_config.dart +++ b/codegen/src/spl_config.dart @@ -105,16 +105,43 @@ void _updateFeatureStatusInConfig(String name, String status) { File(path).writeAsStringSync(result.join('\n')); } -void _updateStorageInConfig(String provider) { +// ─── Storage config helpers ─────────────────────────────────────────────────── + +void _addStorageToConfig(String provider) { + final providers = _getActiveProviders(); + if (providers.contains(provider)) return; + providers.add(provider); + _setActiveStorageInConfig(providers); +} + +void _removeStorageFromConfig(String provider) { + final providers = _getActiveProviders(); + providers.remove(provider); + _setActiveStorageInConfig(providers); +} + +void _setActiveStorageInConfig(List providers) { const path = 'spl.yaml'; File(path).writeAsStringSync( File(path).readAsStringSync().replaceFirst( - RegExp(r'local_backend:.*'), - 'local_backend: $provider', + RegExp(r'active:.*'), + 'active: ${providers.join(',')}', ), ); } +void _updateStorageDefaultInConfig(String provider) { + const path = 'spl.yaml'; + var content = File(path).readAsStringSync(); + // Handle new format (default:) and old format (local_backend:) + if (content.contains(RegExp(r'^\s*default:', multiLine: true))) { + content = content.replaceFirst(RegExp(r'default:.*'), 'default: $provider'); + } else { + content = content.replaceFirst(RegExp(r'local_backend:.*'), 'default: $provider'); + } + File(path).writeAsStringSync(content); +} + void _updateStateDefaultInConfig(String solution) { const path = 'spl.yaml'; File(path).writeAsStringSync( diff --git a/codegen/src/storage_manager.dart b/codegen/src/storage_manager.dart index 1355de7..7170de4 100644 --- a/codegen/src/storage_manager.dart +++ b/codegen/src/storage_manager.dart @@ -3,12 +3,32 @@ part of '../spl_manager.dart'; // ─── Storage impl management ────────────────────────────────────────────────── -String _getActiveProviderName() { - const path = 'lib/core/storage/storage_module.dart'; - if (!File(path).existsSync()) return 'flutter_secure_storage'; - final content = File(path).readAsStringSync(); - final match = RegExp(r'// Active provider: (\S+)').firstMatch(content); - return match?.group(1)?.trim() ?? 'flutter_secure_storage'; +const _validProviders = [ + 'flutter_secure_storage', + 'sqflite', + 'hive', + 'shared_preferences', +]; + +void _validateStorageProvider(String provider) { + if (!_validProviders.contains(provider)) { + _die('Unknown provider: "$provider"\nValid: ${_validProviders.join(' | ')}'); + } +} + +String _getDefaultProviderName() { + final config = _readSplConfig(); + return config['storage']?['default'] as String? + ?? config['storage']?['local_backend'] as String? // backward compat + ?? 'flutter_secure_storage'; +} + +List _getActiveProviders() { + final config = _readSplConfig(); + final raw = config['storage']?['active'] as String? + ?? config['storage']?['local_backend'] as String? // backward compat + ?? 'flutter_secure_storage'; + return raw.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty).toList(); } String _implFileName(String provider) => switch (provider) { @@ -43,40 +63,76 @@ String _storageImplContent(String provider) => switch (provider) { _ => _die('Unknown provider: $provider'), }; -void _rewriteStorageModule(String provider) { - final imports = switch (provider) { - 'flutter_secure_storage' => - "import 'package:flutter_secure_storage/flutter_secure_storage.dart';\nimport 'impl/secure_storage_provider.dart';", - 'sqflite' => "import 'impl/sqflite_storage_provider.dart';", - 'hive' => "import 'impl/hive_storage_provider.dart';", - 'shared_preferences' => "import 'impl/shared_prefs_storage_provider.dart';", - _ => _die('Unknown provider: $provider'), - }; - final providerExpr = switch (provider) { - 'flutter_secure_storage' => 'const SecureStorageProvider(FlutterSecureStorage())', - 'sqflite' => 'SqfliteStorageProvider()', - 'hive' => 'HiveStorageProvider()', - 'shared_preferences' => 'SharedPrefsStorageProvider()', - _ => _die('Unknown provider: $provider'), - }; +/// Ensures [provider] is in the active list and has an impl file. +/// Does NOT run build_runner — caller is responsible for that. +Future _ensureStorageActive(String provider) async { + final activeProviders = _getActiveProviders(); + if (activeProviders.contains(provider)) return; + + print(' Auto-adding storage provider: $provider'); + + if (!File(_implFilePath(provider)).existsSync()) { + final usedMason = await _tryMasonStorage(provider); + if (!usedMason) _generateStorageImpl(provider); + } + + final newProviders = [...activeProviders, provider]; + _addStorageToConfig(provider); + _rewriteStorageModule(newProviders, _getDefaultProviderName()); +} + +String _getterName(String provider) => switch (provider) { + 'flutter_secure_storage' => 'flutterSecureStorage', + 'sqflite' => 'sqflite', + 'hive' => 'hive', + 'shared_preferences' => 'sharedPreferences', + _ => _die('Unknown provider: $provider'), +}; + +String _providerConstructor(String provider) => switch (provider) { + 'flutter_secure_storage' => 'const SecureStorageProvider(FlutterSecureStorage())', + 'sqflite' => 'SqfliteStorageProvider()', + 'hive' => 'HiveStorageProvider()', + 'shared_preferences' => 'SharedPrefsStorageProvider()', + _ => _die('Unknown provider: $provider'), +}; + +void _rewriteStorageModule(List providers, String defaultProvider) { + final importLines = []; + if (providers.contains('flutter_secure_storage')) { + importLines.add("import 'package:flutter_secure_storage/flutter_secure_storage.dart';"); + importLines.add("import 'impl/secure_storage_provider.dart';"); + } + if (providers.contains('sqflite')) + importLines.add("import 'impl/sqflite_storage_provider.dart';"); + if (providers.contains('hive')) + importLines.add("import 'impl/hive_storage_provider.dart';"); + if (providers.contains('shared_preferences')) + importLines.add("import 'impl/shared_prefs_storage_provider.dart';"); + + final getters = providers.map((p) => + " @lazySingleton\n" + " @Named('$p')\n" + " AppStorage get ${_getterName(p)} => ${_providerConstructor(p)};" + ).join('\n\n'); const path = 'lib/core/storage/storage_module.dart'; - File(path).writeAsStringSync('''// ============================================================ + File(path).writeAsStringSync( +'''// ============================================================ // SPL MANAGED FILE — DO NOT EDIT MANUALLY -// Active provider: $provider -// To switch: dart run codegen/spl_manager.dart storage set -// Available: flutter_secure_storage | sqflite | hive | shared_preferences +// Active providers: ${providers.join(', ')} +// Default: $defaultProvider +// To manage: dart run codegen/spl_manager.dart storage add|remove|default // ============================================================ -$imports +${importLines.join('\n')} import 'package:injectable/injectable.dart'; import 'app_storage.dart'; @module abstract class StorageModule { - @lazySingleton - AppStorage get appStorage => $providerExpr; +$getters } '''); print(' ~ lib/core/storage/storage_module.dart (updated)'); diff --git a/codegen/src/templates.dart b/codegen/src/templates.dart index ede5adb..89ab70f 100644 --- a/codegen/src/templates.dart +++ b/codegen/src/templates.dart @@ -288,9 +288,11 @@ class SharedPrefsStorageProvider implements AppStorage { // ─── Data/Domain templates (shared across all state mgmt choices) ───────────── +/// [storageProvider] — the named backend to inject (e.g. 'sqflite'). +/// Null means no local storage for this feature. String _tplLocalDataSources(String module, String className, - {bool withStorage = false}) { - if (!withStorage) { + {String? storageProvider}) { + if (storageProvider == null) { return '''import 'package:injectable/injectable.dart'; abstract class ${className}LocalDataSources {} @@ -313,7 +315,7 @@ abstract class ${className}LocalDataSources { @LazySingleton(as: ${className}LocalDataSources) class ${className}LocalDataSourcesImpl implements ${className}LocalDataSources { final AppStorage _storage; - const ${className}LocalDataSourcesImpl(this._storage); + const ${className}LocalDataSourcesImpl(@Named('$storageProvider') this._storage); @override Future cache(String key, dynamic value) => _storage.put(key, value); diff --git a/codegen/src/utils.dart b/codegen/src/utils.dart index e103d3a..5c61737 100644 --- a/codegen/src/utils.dart +++ b/codegen/src/utils.dart @@ -1,6 +1,67 @@ // ignore_for_file: avoid_print part of '../spl_manager.dart'; +// ─── Per-feature spec parser ────────────────────────────────────────────────── +// +// Parses a feature spec string of the form: +// name[,storage=][,with-storage][,state=][,test][,shell] +// +// Per-feature keys override the global flags passed as defaults. +// Examples: +// "orders" → no options +// "orders,storage=sqflite,state=cubit" → sqflite + cubit override +// "settings,test,shell" → with tests, shell route +// "feed,with-storage" → use global default backend + +({ + String name, + String? storageOverride, + bool withStorage, + String? stateOverride, + bool withTest, + bool shellRoute, +}) _parseFeatureSpec( + String spec, { + String? globalStorageOverride, + bool globalWithStorage = false, + String? globalState, + bool globalWithTest = false, + bool globalShellRoute = false, +}) { + final parts = spec.split(','); + final name = parts[0]; + + String? storageOverride = globalStorageOverride; + bool withStorage = globalWithStorage; + String? stateOverride = globalState; + bool withTest = globalWithTest; + bool shellRoute = globalShellRoute; + + for (final part in parts.skip(1)) { + if (part.startsWith('storage=')) { + storageOverride = part.substring(8); + withStorage = true; + } else if (part == 'with-storage' || part == 'ws') { + withStorage = true; + } else if (part.startsWith('state=')) { + stateOverride = part.substring(6); + } else if (part == 'test') { + withTest = true; + } else if (part == 'shell') { + shellRoute = true; + } + } + + return ( + name: name, + storageOverride: storageOverride, + withStorage: withStorage, + stateOverride: stateOverride, + withTest: withTest, + shellRoute: shellRoute, + ); +} + // ─── Validation + Notes ─────────────────────────────────────────────────────── void _validateStateChoice(String state) { @@ -66,20 +127,36 @@ void _printHelp() { SPL Manager — Software Product Line CLI Variability: - Storage [XOR] one backend for the whole app + Storage [OR] multiple backends can coexist; each feature picks one State Mgmt [OR] global default + per-feature override Commands: list - add Scaffold a new feature (active) - add --with-storage Include local cache (AppStorage) - add --with-test Generate unit + state mgmt tests - add --shell-route Register as shell (bottom nav) route - add --state bloc|cubit|riverpod Override state mgmt for this feature - disable Move to catalog — code kept, DI removed - enable Restore from catalog — DI re-wired - remove [--yes|-y] Hard delete (active or catalog) - storage set Switch storage (XOR) + add [spec2 ...] + Scaffold one or more features. Each spec is: + [,storage=

][,state=][,test][,shell] + + Inline examples: + add orders,storage=sqflite,state=cubit + add feed,test inventory,shell settings + add orders,storage=sqflite feed,with-storage settings + + Global flags (apply to all features unless overridden inline): + --with-storage Use default storage backend + --storage Use a specific backend for all + --with-test Generate tests for all + --shell-route Shell route for all + --state bloc|cubit|riverpod State mgmt for all + + Available providers: flutter_secure_storage | sqflite | hive | shared_preferences + + disable [name2 ...] Move to catalog — code kept, DI removed + enable [name2 ...] Restore from catalog — DI re-wired + remove [name2 ...] [--yes|-y] Hard delete + --yes required when cross-feature deps found + storage add Register a new storage backend + storage remove Unregister a backend + storage default Set default for --with-storage / ,with-storage storage list state set Change default state mgmt state list diff --git a/lib/core/storage/storage_module.dart b/lib/core/storage/storage_module.dart index a2bda2b..ad11728 100644 --- a/lib/core/storage/storage_module.dart +++ b/lib/core/storage/storage_module.dart @@ -1,8 +1,8 @@ // ============================================================ // SPL MANAGED FILE — DO NOT EDIT MANUALLY -// Active provider: flutter_secure_storage -// To switch: dart run codegen/spl_manager.dart storage set -// Available: flutter_secure_storage | sqflite | hive | shared_preferences +// Active providers: flutter_secure_storage +// Default: flutter_secure_storage +// To manage: dart run codegen/spl_manager.dart storage add|remove|default // ============================================================ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -14,5 +14,6 @@ import 'app_storage.dart'; @module abstract class StorageModule { @lazySingleton - AppStorage get appStorage => const SecureStorageProvider(FlutterSecureStorage()); + @Named('flutter_secure_storage') + AppStorage get flutterSecureStorage => const SecureStorageProvider(FlutterSecureStorage()); } diff --git a/lib/services/di.config.dart b/lib/services/di.config.dart index 3f25419..63ef21f 100644 --- a/lib/services/di.config.dart +++ b/lib/services/di.config.dart @@ -75,7 +75,6 @@ extension GetItInjectableX on _i174.GetIt { environmentFilter, ); final storageModule = _$StorageModule(); - gh.lazySingleton<_i812.AppStorage>(() => storageModule.appStorage); gh.lazySingleton<_i124.SecureDatabase>( () => const _i124.SecureDatabaseImpl()); gh.lazySingleton<_i936.NetworkUtils>( @@ -84,6 +83,10 @@ extension GetItInjectableX on _i174.GetIt { () => _i119.DevEnvironment(), registerFor: {_dev}, ); + gh.lazySingleton<_i812.AppStorage>( + () => storageModule.flutterSecureStorage, + instanceName: 'flutter_secure_storage', + ); gh.lazySingleton<_i1024.ProfileLocalDataSources>( () => _i1024.ProfileLocalDataSourcesImpl(gh<_i124.SecureDatabase>())); gh.lazySingleton<_i981.AuthLocalDataSources>( diff --git a/spl.yaml b/spl.yaml index 4030c48..fc2b9ce 100644 --- a/spl.yaml +++ b/spl.yaml @@ -7,11 +7,14 @@ # Commands: # list Show all features + active variants # add [opts] Scaffold a new feature -# --with-storage Include AppStorage-backed local cache +# --with-storage Include AppStorage-backed local cache (uses default) +# --storage Use a specific storage backend # --state bloc|cubit|riverpod Override state management for this feature # remove [--yes|-y] Delete a feature and rebuild DI -# storage set Switch storage backend (exclusive/XOR) -# storage list Show available storage providers +# storage add Register a storage backend +# storage remove Unregister a storage backend +# storage default Set default for --with-storage +# storage list Show active storage providers # state set Change global state management default # state list Show state management options # fix Run build_runner @@ -21,14 +24,14 @@ app: name: boilerplate package: boilerplate -# ── Storage variability (XOR — exactly one active) ─────────────────────────── +# ── Storage variability (OR — multiple backends can coexist) ───────────────── storage: - # Options: flutter_secure_storage | sqflite | hive | shared_preferences - # Only the active provider has an impl file in lib/core/storage/impl/ - local_backend: flutter_secure_storage + # Available: flutter_secure_storage | sqflite | hive | shared_preferences + # Use: dart run codegen/spl_manager.dart storage add|remove|default|list + default: flutter_secure_storage + active: flutter_secure_storage # Always flutter_secure_storage — NOT a variability point - secure_backend: flutter_secure_storage # ── State management variability (OR — global default + per-feature override) ─ state_management: