├── .gitignore ├── .metadata ├── README.md ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── minafarid │ │ │ │ └── complete_advanced_flutter │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-ar │ │ │ └── strings.xml │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ ├── color.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── fonts │ ├── Montserrat-Bold.ttf │ ├── Montserrat-Light.ttf │ ├── Montserrat-Medium.ttf │ ├── Montserrat-Regular.ttf │ └── Montserrat-SemiBold.ttf ├── images │ ├── 1.5x │ │ └── splash_logo.png │ ├── 2.0x │ │ └── splash_logo.png │ ├── 3.0x │ │ └── splash_logo.png │ ├── 4.0x │ │ └── splash_logo.png │ ├── change_lang_ic.svg │ ├── contact_us_ic.svg │ ├── hollow_cirlce_ic.svg │ ├── invite_friends_ic.svg │ ├── left_arrow_ic.svg │ ├── logout_ic.svg │ ├── onboarding_logo1.svg │ ├── onboarding_logo2.svg │ ├── onboarding_logo3.svg │ ├── onboarding_logo4.svg │ ├── photo_camera_ic.svg │ ├── right_arrow_ic.svg │ ├── settings_right_arrow_ic.svg │ ├── solid_circle_ic.svg │ └── splash_logo.png ├── json │ ├── empty.json │ ├── error.json │ ├── loading.json │ └── success.json └── translations │ ├── ar-SA.json │ └── en-US.json ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x-1.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x-1.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x-1.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x-1.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ └── ItunesArtwork@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── app │ ├── app.dart │ ├── app_prefs.dart │ ├── constant.dart │ ├── di.dart │ ├── extensions.dart │ └── functions.dart ├── data │ ├── data_source │ │ ├── local_data_source.dart │ │ └── remote_data_source.dart │ ├── mapper │ │ └── mapper.dart │ ├── network │ │ ├── app_api.dart │ │ ├── app_api.g.dart │ │ ├── dio_factory.dart │ │ ├── error_handler.dart │ │ ├── failure.dart │ │ └── network_info.dart │ ├── repository │ │ └── repository_impl.dart │ ├── request │ │ └── request.dart │ └── responses │ │ ├── responses.dart │ │ └── responses.g.dart ├── domain │ ├── model │ │ └── model.dart │ ├── repository │ │ └── repository.dart │ └── usecase │ │ ├── base_usecase.dart │ │ ├── forgot_password_usecase.dart │ │ ├── home_usecase.dart │ │ ├── login_usecase.dart │ │ ├── register_usecase.dart │ │ └── store_details_usecase.dart ├── main.dart ├── presentation │ ├── base │ │ └── baseviewmodel.dart │ ├── common │ │ ├── freezed_data_classes.dart │ │ ├── freezed_data_classes.freezed.dart │ │ └── state_renderer │ │ │ ├── state_render_impl.dart │ │ │ └── state_renderer.dart │ ├── forgot_password │ │ ├── forgot_password.dart │ │ └── forgot_password_viewmodel.dart │ ├── login │ │ ├── login.dart │ │ └── login_viewmodel.dart │ ├── main │ │ ├── home │ │ │ ├── home_page.dart │ │ │ └── home_viewmodel.dart │ │ ├── main_view.dart │ │ ├── notifications_page.dart │ │ ├── search_page.dart │ │ └── settings_page.dart │ ├── onboarding │ │ ├── onboarding.dart │ │ └── onboarding_viewmodel.dart │ ├── register │ │ ├── register.dart │ │ └── register_viewmodel.dart │ ├── resources │ │ ├── assets_manager.dart │ │ ├── color_manager.dart │ │ ├── font_manager.dart │ │ ├── language_manager.dart │ │ ├── routes_manager.dart │ │ ├── strings_manager.dart │ │ ├── styles_manager.dart │ │ ├── theme_manager.dart │ │ └── values_manager.dart │ ├── splash │ │ └── splash.dart │ └── store_details │ │ ├── store_details.dart │ │ └── store_details_viewmodel.dart └── test.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f4abaa0735eba4dfd8f33f73363911d63931fe03 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # complete_advanced_flutter 2 | 3 | A new Flutter application. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 30 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | defaultConfig { 36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 37 | applicationId "com.minafarid.complete_advanced_flutter" 38 | minSdkVersion 16 39 | targetSdkVersion 30 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 59 | } 60 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 9 | 16 | 20 | 23 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/minafarid/complete_advanced_flutter/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.minafarid.complete_advanced_flutter 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-ar/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | تطبيق توت 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ED9728 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tut App 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /assets/fonts/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/assets/fonts/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Montserrat-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/assets/fonts/Montserrat-Light.ttf -------------------------------------------------------------------------------- /assets/fonts/Montserrat-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/assets/fonts/Montserrat-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/assets/fonts/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/Montserrat-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/assets/fonts/Montserrat-SemiBold.ttf -------------------------------------------------------------------------------- /assets/images/1.5x/splash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/assets/images/1.5x/splash_logo.png -------------------------------------------------------------------------------- /assets/images/2.0x/splash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/assets/images/2.0x/splash_logo.png -------------------------------------------------------------------------------- /assets/images/3.0x/splash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/assets/images/3.0x/splash_logo.png -------------------------------------------------------------------------------- /assets/images/4.0x/splash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/assets/images/4.0x/splash_logo.png -------------------------------------------------------------------------------- /assets/images/change_lang_ic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/images/contact_us_ic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/images/hollow_cirlce_ic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/images/invite_friends_ic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /assets/images/left_arrow_ic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/images/logout_ic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/images/onboarding_logo3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/images/photo_camera_ic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/images/right_arrow_ic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/images/settings_right_arrow_ic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/images/solid_circle_ic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/images/splash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/assets/images/splash_logo.png -------------------------------------------------------------------------------- /assets/translations/ar-SA.json: -------------------------------------------------------------------------------- 1 | { 2 | "no_route_found": "لا توجد شاشه", 3 | "on_boarding_title1": "شاهد افضل كورس #١", 4 | "on_boarding_title2": "شاهد افضل كورس #٢", 5 | "on_boarding_title3": "شاهد افضل كورس #٣", 6 | "on_boarding_title4": "شاهد افضل كورس #٤", 7 | "on_boarding_desc1": "تطبيق توت يكون تطبيق رائع باستخدام اساس منظم #١", 8 | "on_boarding_desc2": "تطبيق توت يكون تطبيق رائع باستخدام اساس منظم #٢", 9 | "on_boarding_desc3": "تطبيق توت يكون تطبيق رائع باستخدام اساس منظم #٣", 10 | "on_boarding_desc4": "تطبيق توت يكون تطبيق رائع باستخدام اساس منظم #٤", 11 | "skip": "تخطي", 12 | "invalid_password": "يجب أن تتكون كلمة المرور من 8 أحرف مع حرف واحد كبير وحرف صغير وحرف رقمي واحد على الأقل", 13 | "username_hint": "اسم المستخدم", 14 | "password_hint": "كلمه السر", 15 | "login_button": "تسجيل الدخول", 16 | "forgot_password_text": "نسيت كلمه السر", 17 | "register_text": "لم تقم بالتسجيل؟ سجل الان", 18 | "loading": "جاري التحميل...", 19 | "retry_again": "حاول مره اخرق", 20 | "error_occurred": "حصل خطآ", 21 | "ok": "موافق", 22 | "invalid_user_name": "اسم المستخدم غير صحيح", 23 | "invalid_mobile_number": "رقم الهاتف غير صحيح", 24 | "invalid_email": "البريد الاليكتروني غير صحيح", 25 | "register": "التسجيل", 26 | "already_have_account": "لديك جساب بالفعل؟ قم بتسجيل الدخول", 27 | "upload_profile_picture": "حمل الصوره الشخصيه", 28 | "photo_gallery": "مكتبه الصور", 29 | "camera": "الكاميرا", 30 | "mobile_number_hint": "رقم الهاتف", 31 | "email_hint": "البريذ الالكتروني", 32 | "notification": "الاشعارات", 33 | "search": "البحث", 34 | "home": "الرىيسيه", 35 | "settings": "الاعدادات", 36 | "services": "الخدمات", 37 | "stores": "المتاجر", 38 | "about": "عن المتجر", 39 | "details": "التفاصيل", 40 | "store_details": "تفصايل المتجر", 41 | "success": "تم النجاح", 42 | "bad_request_error": "طلب غير صالح. حاول مرة أخرى لاحقًا", 43 | "forbidden_error": "طلب محظور. حاول مرة أخرى لاحقًا", 44 | "unauthorized_error": "مستخدم غير مصرح له , حاول مرة أخرى لاحقًا", 45 | "not_found_error": "url غير موجود , حاول مرة أخرى لاحقًا", 46 | "تعارض_خطأ": "تم العثور على تعارض , حاول مرة أخرى لاحقًا", 47 | "Internal_server_error": "حدث خطأ ما , حاول مرة أخرى لاحقًا", 48 | "unknown_error": "حدث خطأ ما , حاول مرة أخرى لاحقًا", 49 | "timeout_error": "انتهت المهلة , حاول مرة أخرى متأخرًا", 50 | "default_error": "حدث خطأ ما , حاول مرة أخرى لاحقًا", 51 | "cache_error": "خطأ في ذاكرة التخزين المؤقت , حاول مرة أخرى لاحقًا", 52 | "no_internet_error": "يُرجى التحقق من اتصالك بالإنترنت", 53 | "change_language": "تغيير اللغة", 54 | "contact_us": "اتصل بنا", 55 | "invite_your_friends": "قم بدعوة أصدقائك", 56 | "logout": "تسجيل الخروج", 57 | "reset_password": "اعاده تعيين كلمه السر" 58 | 59 | } -------------------------------------------------------------------------------- /assets/translations/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "no_route_found": "No Route found", 3 | "on_boarding_title1": "SEE THE BEST COURSE #1", 4 | "on_boarding_title2": "SEE THE BEST COURSE #2", 5 | "on_boarding_title3": "SEE THE BEST COURSE #3", 6 | "on_boarding_title4": "SEE THE BEST COURSE #4", 7 | "on_boarding_desc1": "Tut app is an awesome flutter application using clean architecture #1", 8 | "on_boarding_desc2": "Tut app is an awesome flutter application using clean architecture #2", 9 | "on_boarding_desc3": "Tut app is an awesome flutter application using clean architecture #3", 10 | "on_boarding_desc4": "Tut app is an awesome flutter application using clean architecture #4", 11 | "skip": "Skip", 12 | "invalid_password": "password is wrong", 13 | "username_hint": "UserName", 14 | "password_hint": "Password", 15 | "login_button": "Login", 16 | "forgot_password_text": "Forgot Password", 17 | "register_text": "Not a member ? Sign up", 18 | "loading": "Loading....", 19 | "retry_again": "Retry again", 20 | "error_occurred": "An Error Occurred", 21 | "ok": "Ok", 22 | "invalid_user_name": "Invalid User Name", 23 | "invalid_mobile_number": "Invalid Mobile Number", 24 | "invalid_email": "Invalid Email", 25 | "register": "Register", 26 | "already_have_account": "Already have an account? login", 27 | "upload_profile_pictuSearchre": "profile picture", 28 | "photo_gallery": "Photo from gallery", 29 | "camera": "Photo from camera", 30 | "mobile_number_hint": "Mobile Number", 31 | "email_hint": "Email", 32 | "notification": "Notifications", 33 | "search": "Search", 34 | "home": "Home", 35 | "settings": "Settings", 36 | "services": "Services", 37 | "stores": "Stores", 38 | "about": "About Store", 39 | "details": "Details", 40 | "store_details": "Store Details", 41 | "success": "success", 42 | "bad_request_error": "bad request. try again later", 43 | "forbidden_error": "forbidden request. try again later", 44 | "unauthorized_error": "user unauthorized, try again later", 45 | "not_found_error": "url not found, try again later", 46 | "conflict_error": "conflict found, try again later", 47 | "internal_server_error": "some thing went wrong, try again later", 48 | "unknown_error": "some thing went wrong, try again later", 49 | "timeout_error": "time out, try again late", 50 | "default_error": "some thing went wrong, try again later", 51 | "cache_error": "cache error, try again later", 52 | "no_internet_error": "Please check your internet connection", 53 | "no_content": "success with not content", 54 | "change_language": "Change Language", 55 | "contact_us": "Contact Us", 56 | "invite_your_friends": "Invite Your Friends", 57 | "logout": "Logout", 58 | "reset_password": "Reset Password" 59 | } -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/ephemeral/ 22 | Flutter/app.flx 23 | Flutter/app.zip 24 | Flutter/flutter_assets/ 25 | Flutter/flutter_export_environment.sh 26 | ServiceDefinitions.json 27 | Runner/GeneratedPluginRegistrant.* 28 | 29 | # Exceptions to above rules. 30 | !default.mode1v3 31 | !default.mode2v3 32 | !default.pbxuser 33 | !default.perspectivev3 34 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-App-20x20@2x-1.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "Icon-App-20x20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "Icon-App-29x29@1x-1.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "Icon-App-29x29@2x-1.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "Icon-App-29x29@3x.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "Icon-App-40x40@2x-1.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "Icon-App-40x40@3x.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "Icon-App-60x60@2x.png", 47 | "idiom" : "iphone", 48 | "scale" : "2x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "Icon-App-60x60@3x.png", 53 | "idiom" : "iphone", 54 | "scale" : "3x", 55 | "size" : "60x60" 56 | }, 57 | { 58 | "filename" : "Icon-App-20x20@1x.png", 59 | "idiom" : "ipad", 60 | "scale" : "1x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "Icon-App-20x20@2x.png", 65 | "idiom" : "ipad", 66 | "scale" : "2x", 67 | "size" : "20x20" 68 | }, 69 | { 70 | "filename" : "Icon-App-29x29@1x.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "Icon-App-29x29@2x.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "29x29" 80 | }, 81 | { 82 | "filename" : "Icon-App-40x40@1x.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "Icon-App-40x40@2x.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "40x40" 92 | }, 93 | { 94 | "filename" : "Icon-App-76x76@1x.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "Icon-App-76x76@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "76x76" 104 | }, 105 | { 106 | "filename" : "Icon-App-83.5x83.5@2x.png", 107 | "idiom" : "ipad", 108 | "scale" : "2x", 109 | "size" : "83.5x83.5" 110 | }, 111 | { 112 | "filename" : "ItunesArtwork@2x.png", 113 | "idiom" : "ios-marketing", 114 | "scale" : "1x", 115 | "size" : "1024x1024" 116 | } 117 | ], 118 | "info" : { 119 | "author" : "xcode", 120 | "version" : 1 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x-1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minafarideleia/complete_advanced_flutter/df8528ccf35beae416d0cdef7bc7d476ef3f8d77/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | CFBundleDevelopmentRegion 11 | $(DEVELOPMENT_LANGUAGE) 12 | CFBundleExecutable 13 | $(EXECUTABLE_NAME) 14 | CFBundleIdentifier 15 | $(PRODUCT_BUNDLE_IDENTIFIER) 16 | CFBundleInfoDictionaryVersion 17 | 6.0 18 | CFBundleName 19 | Tut App 20 | CFBundlePackageType 21 | APPL 22 | CFBundleShortVersionString 23 | $(FLUTTER_BUILD_NAME) 24 | CFBundleSignature 25 | ???? 26 | CFBundleVersion 27 | $(FLUTTER_BUILD_NUMBER) 28 | LSRequiresIPhoneOS 29 | 30 | UILaunchStoryboardName 31 | LaunchScreen 32 | UIMainStoryboardFile 33 | Main 34 | UISupportedInterfaceOrientations 35 | 36 | UIInterfaceOrientationPortrait 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UIViewControllerBasedStatusBarAppearance 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/app/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/app/app_prefs.dart'; 2 | import 'package:complete_advanced_flutter/app/di.dart'; 3 | import 'package:complete_advanced_flutter/presentation/resources/routes_manager.dart'; 4 | import 'package:complete_advanced_flutter/presentation/resources/theme_manager.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:easy_localization/easy_localization.dart'; 7 | 8 | class MyApp extends StatefulWidget { 9 | MyApp._internal(); // private named constructor 10 | int appState = 0; 11 | static final MyApp instance = 12 | MyApp._internal(); // single instance -- singleton 13 | 14 | factory MyApp() => instance; // factory for the class instance 15 | 16 | @override 17 | _MyAppState createState() => _MyAppState(); 18 | } 19 | 20 | class _MyAppState extends State { 21 | AppPreferences _appPreferences = instance(); 22 | 23 | @override 24 | void didChangeDependencies() { 25 | _appPreferences.getLocal().then((local) => {context.setLocale(local)}); 26 | 27 | super.didChangeDependencies(); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return MaterialApp( 33 | localizationsDelegates: context.localizationDelegates, 34 | supportedLocales: context.supportedLocales, 35 | locale: context.locale, 36 | debugShowCheckedModeBanner: false, 37 | onGenerateRoute: RouteGenerator.getRoute, 38 | initialRoute: Routes.splashRoute, 39 | theme: getApplicationTheme(), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/app/app_prefs.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/presentation/resources/language_manager.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | 5 | const String PREFS_KEY_LANG = "PREFS_KEY_LANG"; 6 | const String PREFS_KEY_ONBOARDING_SCREEN = "PREFS_KEY_ONBOARDING_SCREEN"; 7 | const String PREFS_KEY_IS_USER_LOGGED_IN = "PREFS_KEY_IS_USER_LOGGED_IN"; 8 | const String PREFS_KEY_TOKEN = "PREFS_KEY_TOKEN"; 9 | 10 | class AppPreferences { 11 | SharedPreferences _sharedPreferences; 12 | 13 | AppPreferences(this._sharedPreferences); 14 | 15 | Future getAppLanguage() async { 16 | String? language = _sharedPreferences.getString(PREFS_KEY_LANG); 17 | 18 | if (language != null && language.isNotEmpty) { 19 | return language; 20 | } else { 21 | return LanguageType.ENGLISH.getValue(); 22 | } 23 | } 24 | 25 | Future setLanguageChanged() async { 26 | String currentLanguage = await getAppLanguage(); 27 | if (currentLanguage == LanguageType.ARABIC.getValue()) { 28 | // save prefs with english lang 29 | _sharedPreferences.setString( 30 | PREFS_KEY_LANG, LanguageType.ENGLISH.getValue()); 31 | } else { 32 | // save prefs with arabic lang 33 | _sharedPreferences.setString( 34 | PREFS_KEY_LANG, LanguageType.ARABIC.getValue()); 35 | } 36 | } 37 | 38 | Future getLocal() async { 39 | String currentLanguage = await getAppLanguage(); 40 | if (currentLanguage == LanguageType.ARABIC.getValue()) { 41 | // return arabic local 42 | return ARABIC_LOCAL; 43 | } else { 44 | // return english local 45 | return ENGLISH_LOCAL; 46 | } 47 | } 48 | 49 | Future setOnBoardingScreenViewed() async { 50 | _sharedPreferences.setBool(PREFS_KEY_ONBOARDING_SCREEN, true); 51 | } 52 | 53 | Future isOnBoardingScreenViewed() async { 54 | return _sharedPreferences.getBool(PREFS_KEY_ONBOARDING_SCREEN) ?? false; 55 | } 56 | 57 | Future setUserToken(String token) async { 58 | _sharedPreferences.setString(PREFS_KEY_TOKEN, token); 59 | } 60 | 61 | Future getUserToken() async { 62 | return _sharedPreferences.getString(PREFS_KEY_TOKEN) ?? ""; 63 | } 64 | 65 | Future setIsUserLoggedIn() async { 66 | _sharedPreferences.setBool(PREFS_KEY_IS_USER_LOGGED_IN, true); 67 | } 68 | 69 | Future isUserLoggedIn() async { 70 | return _sharedPreferences.getBool(PREFS_KEY_IS_USER_LOGGED_IN) ?? false; 71 | } 72 | 73 | Future logout() async { 74 | _sharedPreferences.remove(PREFS_KEY_IS_USER_LOGGED_IN); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/app/constant.dart: -------------------------------------------------------------------------------- 1 | class Constant{ 2 | static const String baseUrl ="http://minafarid123.mocklab.io"; 3 | static const String token ="get api token here"; 4 | } -------------------------------------------------------------------------------- /lib/app/di.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/data/data_source/local_data_source.dart'; 2 | import 'package:complete_advanced_flutter/data/data_source/remote_data_source.dart'; 3 | import 'package:complete_advanced_flutter/data/network/app_api.dart'; 4 | import 'package:complete_advanced_flutter/data/network/dio_factory.dart'; 5 | import 'package:complete_advanced_flutter/data/network/network_info.dart'; 6 | import 'package:complete_advanced_flutter/data/repository/repository_impl.dart'; 7 | import 'package:complete_advanced_flutter/domain/repository/repository.dart'; 8 | import 'package:complete_advanced_flutter/domain/usecase/forgot_password_usecase.dart'; 9 | import 'package:complete_advanced_flutter/domain/usecase/home_usecase.dart'; 10 | import 'package:complete_advanced_flutter/domain/usecase/login_usecase.dart'; 11 | import 'package:complete_advanced_flutter/domain/usecase/register_usecase.dart'; 12 | import 'package:complete_advanced_flutter/domain/usecase/store_details_usecase.dart'; 13 | import 'package:complete_advanced_flutter/presentation/forgot_password/forgot_password_viewmodel.dart'; 14 | import 'package:complete_advanced_flutter/presentation/login/login_viewmodel.dart'; 15 | import 'package:complete_advanced_flutter/presentation/main/home/home_viewmodel.dart'; 16 | import 'package:complete_advanced_flutter/presentation/register/register_viewmodel.dart'; 17 | import 'package:complete_advanced_flutter/presentation/store_details/store_details_viewmodel.dart'; 18 | import 'package:data_connection_checker/data_connection_checker.dart'; 19 | import 'package:get_it/get_it.dart'; 20 | import 'package:image_picker/image_picker.dart'; 21 | import 'package:shared_preferences/shared_preferences.dart'; 22 | 23 | import 'app_prefs.dart'; 24 | 25 | final instance = GetIt.instance; 26 | 27 | Future initAppModule() async { 28 | final sharedPrefs = await SharedPreferences.getInstance(); 29 | 30 | // shared prefs instance 31 | instance.registerLazySingleton(() => sharedPrefs); 32 | 33 | // app prefs instance 34 | instance 35 | .registerLazySingleton(() => AppPreferences(instance())); 36 | 37 | // network info 38 | instance.registerLazySingleton( 39 | () => NetworkInfoImpl(DataConnectionChecker())); 40 | 41 | // dio factory 42 | instance.registerLazySingleton(() => DioFactory(instance())); 43 | 44 | // app service client 45 | final dio = await instance().getDio(); 46 | instance.registerLazySingleton(() => AppServiceClient(dio)); 47 | 48 | // remote data source 49 | instance.registerLazySingleton( 50 | () => RemoteDataSourceImplementer(instance())); 51 | 52 | // local data source 53 | instance.registerLazySingleton( 54 | () => LocalDataSourceImplementer()); 55 | 56 | // repository 57 | instance.registerLazySingleton( 58 | () => RepositoryImpl(instance(), instance(), instance())); 59 | } 60 | 61 | initLoginModule() { 62 | if (!GetIt.I.isRegistered()) { 63 | instance.registerFactory(() => LoginUseCase(instance())); 64 | instance.registerFactory(() => LoginViewModel(instance())); 65 | } 66 | } 67 | 68 | initForgotPasswordModule() { 69 | if (!GetIt.I.isRegistered()) { 70 | instance.registerFactory( 71 | () => ForgotPasswordUseCase(instance())); 72 | instance.registerFactory( 73 | () => ForgotPasswordViewModel(instance())); 74 | } 75 | } 76 | 77 | initRegisterModule() { 78 | if (!GetIt.I.isRegistered()) { 79 | instance 80 | .registerFactory(() => RegisterUseCase(instance())); 81 | instance.registerFactory( 82 | () => RegisterViewModel(instance())); 83 | instance.registerFactory(() => ImagePicker()); 84 | } 85 | } 86 | 87 | initHomeModule() { 88 | if (!GetIt.I.isRegistered()) { 89 | instance.registerFactory(() => HomeUseCase(instance())); 90 | instance.registerFactory(() => HomeViewModel(instance())); 91 | } 92 | } 93 | 94 | initStoreDetailsModule() { 95 | if (!GetIt.I.isRegistered()) { 96 | instance.registerFactory( 97 | () => StoreDetailsUseCase(instance())); 98 | instance.registerFactory( 99 | () => StoreDetailsViewModel(instance())); 100 | } 101 | } 102 | 103 | resetModules() { 104 | instance.reset(dispose: false); 105 | initAppModule(); 106 | initHomeModule(); 107 | initLoginModule(); 108 | initRegisterModule(); 109 | initForgotPasswordModule(); 110 | initStoreDetailsModule(); 111 | } 112 | -------------------------------------------------------------------------------- /lib/app/extensions.dart: -------------------------------------------------------------------------------- 1 | // extension on String 2 | 3 | import 'package:complete_advanced_flutter/data/mapper/mapper.dart'; 4 | 5 | extension NonNullString on String? { 6 | String orEmpty() { 7 | if (this == null) { 8 | return EMPTY; 9 | } else { 10 | return this!; 11 | } 12 | } 13 | } 14 | 15 | // extension on Integer 16 | 17 | extension NonNullInteger on int? { 18 | int orZero() { 19 | if (this == null) { 20 | return ZERO; 21 | } else { 22 | return this!; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/app/functions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:complete_advanced_flutter/domain/model/model.dart'; 4 | import 'package:device_info/device_info.dart'; 5 | import 'package:flutter/services.dart'; 6 | 7 | Future getDeviceDetails() async { 8 | String name = "Unknown"; 9 | String identifier = "Unknown"; 10 | String version = "Unknown"; 11 | 12 | DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); 13 | 14 | try { 15 | if (Platform.isAndroid) { 16 | // return android device info 17 | var build = await deviceInfoPlugin.androidInfo; 18 | name = build.brand + " " + build.model; 19 | identifier = build.androidId; 20 | version = build.version.codename; 21 | } else if (Platform.isIOS) { 22 | // return ios device info 23 | var build = await deviceInfoPlugin.iosInfo; 24 | name = build.name + " " + build.model; 25 | identifier = build.identifierForVendor; 26 | version = build.systemVersion; 27 | } 28 | } on PlatformException { 29 | // return default data here 30 | return DeviceInfo(name, identifier, version); 31 | } 32 | return DeviceInfo(name, identifier, version); 33 | } 34 | 35 | bool isEmailValid(String email) { 36 | return RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+").hasMatch(email); 37 | } -------------------------------------------------------------------------------- /lib/data/data_source/local_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/data/network/error_handler.dart'; 2 | import 'package:complete_advanced_flutter/data/responses/responses.dart'; 3 | 4 | const CACHE_HOME_KEY = "CACHE_HOME_KEY"; 5 | const CACHE_HOME_INTERVAL = 60 * 1000; // 1 MINUTE IN MILLIS 6 | 7 | const CACHE_STORE_DETAILS_KEY = "CACHE_STORE_DETAILS_KEY"; 8 | const CACHE_STORE_DETAILS_INTERVAL = 60 * 1000; // 30s in millis 9 | 10 | abstract class LocalDataSource { 11 | Future getHome(); 12 | 13 | Future saveHomeToCache(HomeResponse homeResponse); 14 | 15 | void clearCache(); 16 | 17 | void removeFromCache(String key); 18 | 19 | Future getStoreDetails(); 20 | 21 | Future saveStoreDetailsToCache(StoreDetailsResponse response); 22 | } 23 | 24 | class LocalDataSourceImplementer implements LocalDataSource { 25 | // run time cache 26 | Map cacheMap = Map(); 27 | 28 | @override 29 | Future getHome() async { 30 | CachedItem? cachedItem = cacheMap[CACHE_HOME_KEY]; 31 | 32 | if (cachedItem != null && cachedItem.isValid(CACHE_HOME_INTERVAL)) { 33 | return cachedItem.data; 34 | // return the response from cache 35 | } else { 36 | // return error that cache is not valid 37 | throw ErrorHandler.handle(DataSource.CACHE_ERROR); 38 | } 39 | } 40 | 41 | @override 42 | Future saveHomeToCache(HomeResponse homeResponse) async { 43 | cacheMap[CACHE_HOME_KEY] = CachedItem(homeResponse); 44 | } 45 | 46 | @override 47 | Future getStoreDetails() async { 48 | CachedItem? cachedItem = cacheMap[CACHE_STORE_DETAILS_KEY]; 49 | 50 | if (cachedItem != null && 51 | cachedItem.isValid(CACHE_STORE_DETAILS_INTERVAL)) { 52 | return cachedItem.data; 53 | } else { 54 | throw ErrorHandler.handle(DataSource.CACHE_ERROR); 55 | } 56 | } 57 | 58 | @override 59 | Future saveStoreDetailsToCache(StoreDetailsResponse response) async { 60 | cacheMap[CACHE_STORE_DETAILS_KEY] = CachedItem(response); 61 | } 62 | 63 | @override 64 | void clearCache() { 65 | cacheMap.clear(); 66 | } 67 | 68 | @override 69 | void removeFromCache(String key) { 70 | cacheMap.remove(key); 71 | } 72 | } 73 | 74 | class CachedItem { 75 | dynamic data; 76 | 77 | int cacheTime = DateTime.now().millisecondsSinceEpoch; 78 | 79 | CachedItem(this.data); 80 | } 81 | 82 | extension CachedItemExtension on CachedItem { 83 | bool isValid(int expirationTime) { 84 | // expirationTime is 60 secs 85 | int currentTimeInMillis = 86 | DateTime.now().millisecondsSinceEpoch; // time now is 1:00:00 pm 87 | 88 | bool isCacheValid = currentTimeInMillis - expirationTime < 89 | cacheTime; // cache time was in 12:59:30 90 | // false if current time > 1:00:30 91 | // true if current time <1:00:30 92 | return isCacheValid; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/data/data_source/remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/data/network/app_api.dart'; 2 | import 'package:complete_advanced_flutter/data/request/request.dart'; 3 | import 'package:complete_advanced_flutter/data/responses/responses.dart'; 4 | 5 | abstract class RemoteDataSource { 6 | Future login(LoginRequest loginRequest); 7 | 8 | Future register(RegisterRequest registerRequest); 9 | 10 | Future forgotPassword(String email); 11 | 12 | Future getHome(); 13 | 14 | Future getStoreDetails(); 15 | } 16 | 17 | class RemoteDataSourceImplementer implements RemoteDataSource { 18 | AppServiceClient _appServiceClient; 19 | 20 | RemoteDataSourceImplementer(this._appServiceClient); 21 | 22 | @override 23 | Future login(LoginRequest loginRequest) async { 24 | return await _appServiceClient.login( 25 | loginRequest.email, loginRequest.password, "", ""); 26 | } 27 | 28 | @override 29 | Future forgotPassword(String email) async { 30 | return await _appServiceClient.forgotPassword(email); 31 | } 32 | 33 | @override 34 | Future register( 35 | RegisterRequest registerRequest) async { 36 | return await _appServiceClient.register( 37 | registerRequest.countryMobileCode, 38 | registerRequest.userName, 39 | registerRequest.email, 40 | registerRequest.password, 41 | registerRequest.mobileNumber, 42 | ""); 43 | } 44 | 45 | @override 46 | Future getHome() async { 47 | return await _appServiceClient.getHome(); 48 | } 49 | 50 | @override 51 | Future getStoreDetails() async { 52 | return await _appServiceClient.getStoreDetails(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/data/mapper/mapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/app/extensions.dart'; 2 | 3 | // to convert the response into a non nullable object (model) 4 | 5 | import 'package:complete_advanced_flutter/data/responses/responses.dart'; 6 | import 'package:complete_advanced_flutter/domain/model/model.dart'; 7 | 8 | const EMPTY = ""; 9 | const ZERO = 0; 10 | 11 | extension CustomerResponseMapper on CustomerResponse? { 12 | Customer toDomain() { 13 | return Customer( 14 | this?.id?.orEmpty() ?? EMPTY, 15 | this?.name?.orEmpty() ?? EMPTY, 16 | this?.numOfNotifications?.orZero() ?? ZERO); 17 | } 18 | } 19 | 20 | extension ContactsResponseMapper on ContactsResponse? { 21 | Contacts toDomain() { 22 | return Contacts(this?.email?.orEmpty() ?? EMPTY, 23 | this?.phone?.orEmpty() ?? EMPTY, this?.link?.orEmpty() ?? EMPTY); 24 | } 25 | } 26 | 27 | extension AuthenticationResponseMapper on AuthenticationResponse? { 28 | Authentication toDomain() { 29 | return Authentication( 30 | this?.customer?.toDomain(), this?.contacts?.toDomain()); 31 | } 32 | } 33 | 34 | extension ForgotPasswordResponseMapper on ForgotPasswordResponse? { 35 | String toDomain() { 36 | return this?.support?.orEmpty() ?? EMPTY; 37 | } 38 | } 39 | 40 | extension ServiceResponseMapper on ServiceResponse? { 41 | Service toDomain() { 42 | return Service(this?.id?.orZero() ?? ZERO, this?.title?.orEmpty() ?? EMPTY, 43 | this?.image?.orEmpty() ?? EMPTY); 44 | } 45 | } 46 | 47 | extension StoreResponseMapper on StoreResponse? { 48 | Store toDomain() { 49 | return Store(this?.id?.orZero() ?? ZERO, this?.title?.orEmpty() ?? EMPTY, 50 | this?.image?.orEmpty() ?? EMPTY); 51 | } 52 | } 53 | 54 | extension BannerResponseMapper on BannerResponse? { 55 | BannerAd toDomain() { 56 | return BannerAd(this?.id?.orZero() ?? ZERO, this?.title?.orEmpty() ?? EMPTY, 57 | this?.image?.orEmpty() ?? EMPTY, this?.link?.orEmpty() ?? EMPTY); 58 | } 59 | } 60 | 61 | extension HomeResponseMapper on HomeResponse? { 62 | HomeObject toDomain() { 63 | List mappedServices = 64 | (this?.data?.services?.map((service) => service.toDomain()) ?? 65 | Iterable.empty()) 66 | .cast() 67 | .toList(); 68 | 69 | List mappedStores = 70 | (this?.data?.stores?.map((store) => store.toDomain()) ?? 71 | Iterable.empty()) 72 | .cast() 73 | .toList(); 74 | 75 | List mappedBanners = 76 | (this?.data?.banners?.map((bannerAd) => bannerAd.toDomain()) ?? 77 | Iterable.empty()) 78 | .cast() 79 | .toList(); 80 | 81 | var data = HomeData(mappedServices, mappedStores, mappedBanners); 82 | return HomeObject(data); 83 | } 84 | } 85 | extension StoreDetailsResponseMapper on StoreDetailsResponse? { 86 | StoreDetails toDomain() { 87 | return StoreDetails( 88 | this?.id?.orZero() ?? ZERO, 89 | this?.title?.orEmpty() ?? EMPTY, 90 | this?.image?.orEmpty() ?? EMPTY, 91 | this?.details?.orEmpty() ?? EMPTY, 92 | this?.services?.orEmpty() ?? EMPTY, 93 | this?.about?.orEmpty() ?? EMPTY); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/data/network/app_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/app/constant.dart'; 2 | import 'package:complete_advanced_flutter/data/responses/responses.dart'; 3 | import 'package:dio/dio.dart'; 4 | import 'package:retrofit/http.dart'; 5 | 6 | part 'app_api.g.dart'; 7 | 8 | @RestApi(baseUrl: Constant.baseUrl) 9 | abstract class AppServiceClient { 10 | factory AppServiceClient(Dio dio, {String baseUrl}) = _AppServiceClient; 11 | 12 | @POST("/customers/login") 13 | Future login( 14 | @Field("email") String email, 15 | @Field("password") String password, 16 | @Field("imei") String imei, 17 | @Field("deviceType") String deviceType, 18 | ); 19 | 20 | @POST("/customers/forgotPassword") 21 | Future forgotPassword(@Field("email") String email); 22 | 23 | @POST("/customers/register") 24 | Future register( 25 | @Field("country_mobile_code") String countryMobileCode, 26 | @Field("user_name") String userName, 27 | @Field("email") String email, 28 | @Field("password") String password, 29 | @Field("mobile_number") String mobilNumber, 30 | @Field("profile_picture") String profilePicture, 31 | ); 32 | 33 | @GET("/home") 34 | Future getHome(); 35 | 36 | @GET("/storeDetails/1") 37 | Future getStoreDetails(); 38 | } 39 | -------------------------------------------------------------------------------- /lib/data/network/app_api.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'app_api.dart'; 4 | 5 | // ************************************************************************** 6 | // RetrofitGenerator 7 | // ************************************************************************** 8 | 9 | class _AppServiceClient implements AppServiceClient { 10 | _AppServiceClient(this._dio, {this.baseUrl}) { 11 | baseUrl ??= 'http://minafarid123.mocklab.io'; 12 | } 13 | 14 | final Dio _dio; 15 | 16 | String? baseUrl; 17 | 18 | @override 19 | Future login( 20 | email, password, imei, deviceType) async { 21 | const _extra = {}; 22 | final queryParameters = {}; 23 | final _data = { 24 | 'email': email, 25 | 'password': password, 26 | 'imei': imei, 27 | 'deviceType': deviceType 28 | }; 29 | final _result = await _dio.fetch>( 30 | _setStreamType( 31 | Options(method: 'POST', headers: {}, extra: _extra) 32 | .compose(_dio.options, '/customers/login', 33 | queryParameters: queryParameters, data: _data) 34 | .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl))); 35 | final value = AuthenticationResponse.fromJson(_result.data!); 36 | return value; 37 | } 38 | 39 | @override 40 | Future forgotPassword(email) async { 41 | const _extra = {}; 42 | final queryParameters = {}; 43 | final _data = {'email': email}; 44 | final _result = await _dio.fetch>( 45 | _setStreamType( 46 | Options(method: 'POST', headers: {}, extra: _extra) 47 | .compose(_dio.options, '/customers/forgotPassword', 48 | queryParameters: queryParameters, data: _data) 49 | .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl))); 50 | final value = ForgotPasswordResponse.fromJson(_result.data!); 51 | return value; 52 | } 53 | 54 | @override 55 | Future register(countryMobileCode, userName, email, 56 | password, mobilNumber, profilePicture) async { 57 | const _extra = {}; 58 | final queryParameters = {}; 59 | final _data = { 60 | 'country_mobile_code': countryMobileCode, 61 | 'user_name': userName, 62 | 'email': email, 63 | 'password': password, 64 | 'mobile_number': mobilNumber, 65 | 'profile_picture': profilePicture 66 | }; 67 | final _result = await _dio.fetch>( 68 | _setStreamType( 69 | Options(method: 'POST', headers: {}, extra: _extra) 70 | .compose(_dio.options, '/customers/register', 71 | queryParameters: queryParameters, data: _data) 72 | .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl))); 73 | final value = AuthenticationResponse.fromJson(_result.data!); 74 | return value; 75 | } 76 | 77 | @override 78 | Future getHome() async { 79 | const _extra = {}; 80 | final queryParameters = {}; 81 | final _data = {}; 82 | final _result = await _dio.fetch>( 83 | _setStreamType( 84 | Options(method: 'GET', headers: {}, extra: _extra) 85 | .compose(_dio.options, '/home', 86 | queryParameters: queryParameters, data: _data) 87 | .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl))); 88 | final value = HomeResponse.fromJson(_result.data!); 89 | return value; 90 | } 91 | 92 | @override 93 | Future getStoreDetails() async { 94 | const _extra = {}; 95 | final queryParameters = {}; 96 | final _data = {}; 97 | final _result = await _dio.fetch>( 98 | _setStreamType( 99 | Options(method: 'GET', headers: {}, extra: _extra) 100 | .compose(_dio.options, '/storeDetails/1', 101 | queryParameters: queryParameters, data: _data) 102 | .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl))); 103 | final value = StoreDetailsResponse.fromJson(_result.data!); 104 | return value; 105 | } 106 | 107 | RequestOptions _setStreamType(RequestOptions requestOptions) { 108 | if (T != dynamic && 109 | !(requestOptions.responseType == ResponseType.bytes || 110 | requestOptions.responseType == ResponseType.stream)) { 111 | if (T == String) { 112 | requestOptions.responseType = ResponseType.plain; 113 | } else { 114 | requestOptions.responseType = ResponseType.json; 115 | } 116 | } 117 | return requestOptions; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/data/network/dio_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/app/app_prefs.dart'; 2 | import 'package:complete_advanced_flutter/app/constant.dart'; 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:pretty_dio_logger/pretty_dio_logger.dart'; 6 | 7 | const String APPLICATION_JSON = "application/json"; 8 | const String CONTENT_TYPE = "content-type"; 9 | const String ACCEPT = "accept"; 10 | const String AUTHORIZATION = "authorization"; 11 | const String DEFAULT_LANGUAGE = "language"; 12 | 13 | class DioFactory { 14 | AppPreferences _appPreferences; 15 | 16 | DioFactory(this._appPreferences); 17 | 18 | Future getDio() async { 19 | Dio dio = Dio(); 20 | int _timeOut = 60 * 1000; // 1 min 21 | String language = await _appPreferences.getAppLanguage(); 22 | String token = await _appPreferences.getUserToken(); 23 | Map headers = { 24 | CONTENT_TYPE: APPLICATION_JSON, 25 | ACCEPT: APPLICATION_JSON, 26 | AUTHORIZATION: token, 27 | DEFAULT_LANGUAGE: language 28 | }; 29 | 30 | dio.options = BaseOptions( 31 | baseUrl: Constant.baseUrl, 32 | connectTimeout: _timeOut, 33 | receiveTimeout: _timeOut, 34 | headers: headers); 35 | 36 | if (kReleaseMode) { 37 | print("release mode no logs"); 38 | } else { 39 | dio.interceptors.add(PrettyDioLogger( 40 | requestHeader: true, requestBody: true, responseHeader: true)); 41 | } 42 | 43 | return dio; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/data/network/error_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/data/network/failure.dart'; 2 | import 'package:complete_advanced_flutter/presentation/resources/strings_manager.dart'; 3 | import 'package:dio/dio.dart'; 4 | import 'package:easy_localization/easy_localization.dart'; 5 | 6 | enum DataSource { 7 | SUCCESS, 8 | NO_CONTENT, 9 | BAD_REQUEST, 10 | FORBIDDEN, 11 | UNAUTHORISED, 12 | NOT_FOUND, 13 | INTERNAL_SERVER_ERROR, 14 | CONNECT_TIMEOUT, 15 | CANCEL, 16 | RECEIVE_TIMEOUT, 17 | SEND_TIMEOUT, 18 | CACHE_ERROR, 19 | NO_INTERNET_CONNECTION, 20 | DEFAULT 21 | } 22 | 23 | class ErrorHandler implements Exception { 24 | late Failure failure; 25 | 26 | ErrorHandler.handle(dynamic error) { 27 | if (error is DioError) { 28 | // dio error so its error from response of the API 29 | failure = _handleError(error); 30 | } else { 31 | // default error 32 | failure = DataSource.DEFAULT.getFailure(); 33 | } 34 | } 35 | 36 | Failure _handleError(DioError error) { 37 | switch (error.type) { 38 | case DioErrorType.connectTimeout: 39 | return DataSource.CONNECT_TIMEOUT.getFailure(); 40 | case DioErrorType.sendTimeout: 41 | return DataSource.SEND_TIMEOUT.getFailure(); 42 | case DioErrorType.receiveTimeout: 43 | return DataSource.RECEIVE_TIMEOUT.getFailure(); 44 | case DioErrorType.response: 45 | switch (error.response?.statusCode) { 46 | case ResponseCode.BAD_REQUEST: 47 | return DataSource.BAD_REQUEST.getFailure(); 48 | case ResponseCode.FORBIDDEN: 49 | return DataSource.FORBIDDEN.getFailure(); 50 | case ResponseCode.UNAUTHORISED: 51 | return DataSource.UNAUTHORISED.getFailure(); 52 | case ResponseCode.NOT_FOUND: 53 | return DataSource.NOT_FOUND.getFailure(); 54 | case ResponseCode.INTERNAL_SERVER_ERROR: 55 | return DataSource.INTERNAL_SERVER_ERROR.getFailure(); 56 | default: 57 | return DataSource.DEFAULT.getFailure(); 58 | } 59 | case DioErrorType.cancel: 60 | return DataSource.CANCEL.getFailure(); 61 | case DioErrorType.other: 62 | return DataSource.DEFAULT.getFailure(); 63 | } 64 | } 65 | } 66 | 67 | extension DataSourceExtension on DataSource { 68 | Failure getFailure() { 69 | switch (this) { 70 | case DataSource.BAD_REQUEST: 71 | return Failure(ResponseCode.BAD_REQUEST, ResponseMessage.BAD_REQUEST.tr()); 72 | case DataSource.FORBIDDEN: 73 | return Failure(ResponseCode.FORBIDDEN, ResponseMessage.FORBIDDEN.tr()); 74 | case DataSource.UNAUTHORISED: 75 | return Failure(ResponseCode.UNAUTHORISED, ResponseMessage.UNAUTHORISED.tr()); 76 | case DataSource.NOT_FOUND: 77 | return Failure(ResponseCode.NOT_FOUND, ResponseMessage.NOT_FOUND.tr()); 78 | case DataSource.INTERNAL_SERVER_ERROR: 79 | return Failure(ResponseCode.INTERNAL_SERVER_ERROR, 80 | ResponseMessage.INTERNAL_SERVER_ERROR.tr()); 81 | case DataSource.CONNECT_TIMEOUT: 82 | return Failure( 83 | ResponseCode.CONNECT_TIMEOUT, ResponseMessage.CONNECT_TIMEOUT.tr()); 84 | case DataSource.CANCEL: 85 | return Failure(ResponseCode.CANCEL, ResponseMessage.CANCEL.tr()); 86 | case DataSource.RECEIVE_TIMEOUT: 87 | return Failure( 88 | ResponseCode.RECEIVE_TIMEOUT, ResponseMessage.RECEIVE_TIMEOUT.tr()); 89 | case DataSource.SEND_TIMEOUT: 90 | return Failure(ResponseCode.SEND_TIMEOUT, ResponseMessage.SEND_TIMEOUT.tr()); 91 | case DataSource.CACHE_ERROR: 92 | return Failure(ResponseCode.CACHE_ERROR, ResponseMessage.CACHE_ERROR.tr()); 93 | case DataSource.NO_INTERNET_CONNECTION: 94 | return Failure(ResponseCode.NO_INTERNET_CONNECTION, 95 | ResponseMessage.NO_INTERNET_CONNECTION.tr()); 96 | case DataSource.DEFAULT: 97 | return Failure(ResponseCode.DEFAULT, ResponseMessage.DEFAULT); 98 | default: 99 | return Failure(ResponseCode.DEFAULT, ResponseMessage.DEFAULT); 100 | } 101 | } 102 | } 103 | 104 | class ResponseCode { 105 | // API status codes 106 | static const int SUCCESS = 200; // success with data 107 | static const int NO_CONTENT = 201; // success with no content 108 | static const int BAD_REQUEST = 400; // failure, api rejected the request 109 | static const int FORBIDDEN = 403; // failure, api rejected the request 110 | static const int UNAUTHORISED = 401; // failure user is not authorised 111 | static const int NOT_FOUND = 112 | 404; // failure, api url is not correct and not found 113 | static const int INTERNAL_SERVER_ERROR = 114 | 500; // failure, crash happened in server side 115 | 116 | // local status code 117 | static const int DEFAULT = -1; 118 | static const int CONNECT_TIMEOUT = -2; 119 | static const int CANCEL = -3; 120 | static const int RECEIVE_TIMEOUT = -4; 121 | static const int SEND_TIMEOUT = -5; 122 | static const int CACHE_ERROR = -6; 123 | static const int NO_INTERNET_CONNECTION = -7; 124 | } 125 | 126 | class ResponseMessage { 127 | // API status codes 128 | // API response codes 129 | static const String SUCCESS = AppStrings.success; // success with data 130 | static const String NO_CONTENT = 131 | AppStrings.noContent; // success with no content 132 | static const String BAD_REQUEST = 133 | AppStrings.badRequestError; // failure, api rejected our request 134 | static const String FORBIDDEN = 135 | AppStrings.forbiddenError; // failure, api rejected our request 136 | static const String UNAUTHORISED = 137 | AppStrings.unauthorizedError; // failure, user is not authorised 138 | static const String NOT_FOUND = AppStrings 139 | .notFoundError; // failure, API url is not correct and not found in api side. 140 | static const String INTERNAL_SERVER_ERROR = 141 | AppStrings.internalServerError; // failure, a crash happened in API side. 142 | 143 | // local responses codes 144 | static const String DEFAULT = 145 | AppStrings.defaultError; // unknown error happened 146 | static const String CONNECT_TIMEOUT = 147 | AppStrings.timeoutError; // issue in connectivity 148 | static const String CANCEL = 149 | AppStrings.defaultError; // API request was cancelled 150 | static const String RECEIVE_TIMEOUT = 151 | AppStrings.timeoutError; // issue in connectivity 152 | static const String SEND_TIMEOUT = 153 | AppStrings.timeoutError; // issue in connectivity 154 | static const String CACHE_ERROR = AppStrings 155 | .defaultError; // issue in getting data from local data source (cache) 156 | static const String NO_INTERNET_CONNECTION = 157 | AppStrings.noInternetError; // issue in connectivity 158 | } 159 | 160 | class ApiInternalStatus { 161 | static const int SUCCESS = 0; 162 | static const int FAILURE = 1; 163 | } 164 | -------------------------------------------------------------------------------- /lib/data/network/failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/data/network/error_handler.dart'; 2 | 3 | class Failure { 4 | int code; // 200 or 400 5 | String message; // error or success 6 | 7 | Failure(this.code, this.message); 8 | } 9 | 10 | class DefaultFailure extends Failure { 11 | DefaultFailure() : super(ResponseCode.DEFAULT, ResponseMessage.DEFAULT); 12 | } 13 | -------------------------------------------------------------------------------- /lib/data/network/network_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:data_connection_checker/data_connection_checker.dart'; 2 | 3 | abstract class NetworkInfo { 4 | Future get isConnected; 5 | } 6 | 7 | class NetworkInfoImpl implements NetworkInfo { 8 | DataConnectionChecker _dataConnectionChecker; 9 | 10 | NetworkInfoImpl(this._dataConnectionChecker); 11 | 12 | @override 13 | Future get isConnected => _dataConnectionChecker.hasConnection; 14 | } 15 | -------------------------------------------------------------------------------- /lib/data/repository/repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/data/data_source/local_data_source.dart'; 2 | import 'package:complete_advanced_flutter/data/data_source/remote_data_source.dart'; 3 | import 'package:complete_advanced_flutter/data/network/error_handler.dart'; 4 | import 'package:complete_advanced_flutter/data/network/failure.dart'; 5 | import 'package:complete_advanced_flutter/data/network/network_info.dart'; 6 | import 'package:complete_advanced_flutter/data/request/request.dart'; 7 | import 'package:complete_advanced_flutter/domain/model/model.dart'; 8 | import 'package:complete_advanced_flutter/domain/repository/repository.dart'; 9 | import 'package:dartz/dartz.dart'; 10 | import 'package:complete_advanced_flutter/data/mapper/mapper.dart'; 11 | 12 | class RepositoryImpl extends Repository { 13 | RemoteDataSource _remoteDataSource; 14 | LocalDataSource _localDataSource; 15 | NetworkInfo _networkInfo; 16 | 17 | RepositoryImpl( 18 | this._remoteDataSource, this._localDataSource, this._networkInfo); 19 | 20 | @override 21 | Future> login( 22 | LoginRequest loginRequest) async { 23 | if (await _networkInfo.isConnected) { 24 | try { 25 | // its safe to call the API 26 | final response = await _remoteDataSource.login(loginRequest); 27 | 28 | if (response.status == ApiInternalStatus.SUCCESS) // success 29 | { 30 | // return data (success) 31 | // return right 32 | return Right(response.toDomain()); 33 | } else { 34 | // return biz logic error 35 | // return left 36 | return Left(Failure(response.status ?? ApiInternalStatus.FAILURE, 37 | response.message ?? ResponseMessage.DEFAULT)); 38 | } 39 | } catch (error) { 40 | return (Left(ErrorHandler.handle(error).failure)); 41 | } 42 | } else { 43 | // return connection error 44 | return Left(DataSource.NO_INTERNET_CONNECTION.getFailure()); 45 | } 46 | } 47 | 48 | @override 49 | Future> forgotPassword(String email) async { 50 | if (await _networkInfo.isConnected) { 51 | try { 52 | // its safe to call API 53 | final response = await _remoteDataSource.forgotPassword(email); 54 | 55 | if (response.status == ApiInternalStatus.SUCCESS) { 56 | // success 57 | // return right 58 | return Right(response.toDomain()); 59 | } else { 60 | // failure 61 | // return left 62 | return Left(Failure(response.status ?? ResponseCode.DEFAULT, 63 | response.message ?? ResponseMessage.DEFAULT)); 64 | } 65 | } catch (error) { 66 | return Left(ErrorHandler.handle(error).failure); 67 | } 68 | } else { 69 | // return network connection error 70 | // return left 71 | return Left(DataSource.NO_INTERNET_CONNECTION.getFailure()); 72 | } 73 | } 74 | 75 | @override 76 | Future> register( 77 | RegisterRequest registerRequest) async { 78 | if (await _networkInfo.isConnected) { 79 | try { 80 | // its safe to call the API 81 | final response = await _remoteDataSource.register(registerRequest); 82 | 83 | if (response.status == ApiInternalStatus.SUCCESS) // success 84 | { 85 | // return data (success) 86 | // return right 87 | return Right(response.toDomain()); 88 | } else { 89 | // return biz logic error 90 | // return left 91 | return Left(Failure(response.status ?? ApiInternalStatus.FAILURE, 92 | response.message ?? ResponseMessage.DEFAULT)); 93 | } 94 | } catch (error) { 95 | return (Left(ErrorHandler.handle(error).failure)); 96 | } 97 | } else { 98 | // return connection error 99 | return Left(DataSource.NO_INTERNET_CONNECTION.getFailure()); 100 | } 101 | } 102 | 103 | @override 104 | Future> getHome() async { 105 | try { 106 | // get from cache 107 | final response = await _localDataSource.getHome(); 108 | return Right(response.toDomain()); 109 | } catch (cacheError) { 110 | // we have cache error so we should call API 111 | 112 | if (await _networkInfo.isConnected) { 113 | try { 114 | // its safe to call the API 115 | final response = await _remoteDataSource.getHome(); 116 | 117 | if (response.status == ApiInternalStatus.SUCCESS) // success 118 | { 119 | // return data (success) 120 | // return right 121 | // save response to local data source 122 | _localDataSource.saveHomeToCache(response); 123 | return Right(response.toDomain()); 124 | } else { 125 | // return biz logic error 126 | // return left 127 | return Left(Failure(response.status ?? ApiInternalStatus.FAILURE, 128 | response.message ?? ResponseMessage.DEFAULT)); 129 | } 130 | } catch (error) { 131 | return (Left(ErrorHandler.handle(error).failure)); 132 | } 133 | } else { 134 | // return connection error 135 | return Left(DataSource.NO_INTERNET_CONNECTION.getFailure()); 136 | } 137 | } 138 | } 139 | 140 | @override 141 | Future> getStoreDetails() async { 142 | try { 143 | // get data from cache 144 | 145 | final response = await _localDataSource.getStoreDetails(); 146 | return Right(response.toDomain()); 147 | } catch (cacheError) { 148 | if (await _networkInfo.isConnected) { 149 | try { 150 | final response = await _remoteDataSource.getStoreDetails(); 151 | if (response.status == ApiInternalStatus.SUCCESS) { 152 | _localDataSource.saveStoreDetailsToCache(response); 153 | return Right(response.toDomain()); 154 | } else { 155 | return Left(Failure(response.status ?? ResponseCode.DEFAULT, 156 | response.message ?? ResponseMessage.DEFAULT)); 157 | } 158 | } catch (error) { 159 | return Left(ErrorHandler.handle(error).failure); 160 | } 161 | } else { 162 | return Left(DataSource.NO_INTERNET_CONNECTION.getFailure()); 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /lib/data/request/request.dart: -------------------------------------------------------------------------------- 1 | class LoginRequest { 2 | String email; 3 | String password; 4 | String imei; 5 | String deviceType; 6 | 7 | LoginRequest(this.email, this.password, this.imei, this.deviceType); 8 | } 9 | 10 | class RegisterRequest { 11 | String countryMobileCode; 12 | String userName; 13 | String email; 14 | String password; 15 | String profilePicture; 16 | String mobileNumber; 17 | 18 | RegisterRequest(this.countryMobileCode, this.userName, this.email, 19 | this.password, this.profilePicture,this.mobileNumber); 20 | } 21 | -------------------------------------------------------------------------------- /lib/data/responses/responses.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'responses.g.dart'; 4 | 5 | @JsonSerializable() 6 | class BaseResponse { 7 | @JsonKey(name: "status") 8 | int? status; 9 | @JsonKey(name: "message") 10 | String? message; 11 | } 12 | 13 | @JsonSerializable() 14 | class CustomerResponse { 15 | @JsonKey(name: "id") 16 | String? id; 17 | @JsonKey(name: "name") 18 | String? name; 19 | @JsonKey(name: "numOfNotifications") 20 | int? numOfNotifications; 21 | 22 | CustomerResponse(this.id, this.name, this.numOfNotifications); 23 | 24 | // from json 25 | factory CustomerResponse.fromJson(Map json) => 26 | _$CustomerResponseFromJson(json); 27 | 28 | // to json 29 | Map toJson() => _$CustomerResponseToJson(this); 30 | } 31 | 32 | @JsonSerializable() 33 | class ContactsResponse { 34 | @JsonKey(name: "email") 35 | String? email; 36 | @JsonKey(name: "phone") 37 | String? phone; 38 | @JsonKey(name: "link") 39 | String? link; 40 | 41 | ContactsResponse(this.email, this.phone, this.link); 42 | 43 | // from json 44 | factory ContactsResponse.fromJson(Map json) => 45 | _$ContactsResponseFromJson(json); 46 | 47 | // to json 48 | Map toJson() => _$ContactsResponseToJson(this); 49 | } 50 | 51 | @JsonSerializable() 52 | class AuthenticationResponse extends BaseResponse { 53 | @JsonKey(name: "customer") 54 | CustomerResponse? customer; 55 | @JsonKey(name: "contacts") 56 | ContactsResponse? contacts; 57 | 58 | AuthenticationResponse(this.customer, this.contacts); 59 | 60 | // from json 61 | factory AuthenticationResponse.fromJson(Map json) => 62 | _$AuthenticationResponseFromJson(json); 63 | 64 | // to json 65 | Map toJson() => _$AuthenticationResponseToJson(this); 66 | } 67 | 68 | @JsonSerializable() 69 | class ForgotPasswordResponse extends BaseResponse { 70 | @JsonKey(name: 'support') 71 | String? support; 72 | 73 | ForgotPasswordResponse(this.support); 74 | 75 | // toJson 76 | Map toJson() => _$ForgotPasswordResponseToJson(this); 77 | 78 | //fromJson 79 | factory ForgotPasswordResponse.fromJson(Map json) => 80 | _$ForgotPasswordResponseFromJson(json); 81 | } 82 | 83 | @JsonSerializable() 84 | class ServiceResponse { 85 | @JsonKey(name: 'id') 86 | int? id; 87 | @JsonKey(name: 'title') 88 | String? title; 89 | @JsonKey(name: 'image') 90 | String? image; 91 | 92 | ServiceResponse(this.id, this.title, this.image); 93 | 94 | // toJson 95 | Map toJson() => _$ServiceResponseToJson(this); 96 | 97 | //fromJson 98 | factory ServiceResponse.fromJson(Map json) => 99 | _$ServiceResponseFromJson(json); 100 | } 101 | 102 | @JsonSerializable() 103 | class StoreResponse { 104 | @JsonKey(name: 'id') 105 | int? id; 106 | @JsonKey(name: 'title') 107 | String? title; 108 | @JsonKey(name: 'image') 109 | String? image; 110 | 111 | StoreResponse(this.id, this.title, this.image); 112 | 113 | // toJson 114 | Map toJson() => _$StoreResponseToJson(this); 115 | 116 | //fromJson 117 | factory StoreResponse.fromJson(Map json) => 118 | _$StoreResponseFromJson(json); 119 | } 120 | 121 | @JsonSerializable() 122 | class BannerResponse { 123 | @JsonKey(name: 'id') 124 | int? id; 125 | @JsonKey(name: 'title') 126 | String? title; 127 | @JsonKey(name: 'image') 128 | String? image; 129 | @JsonKey(name: 'link') 130 | String? link; 131 | 132 | BannerResponse(this.id, this.title, this.image, this.link); 133 | 134 | // toJson 135 | Map toJson() => _$BannerResponseToJson(this); 136 | 137 | //fromJson 138 | factory BannerResponse.fromJson(Map json) => 139 | _$BannerResponseFromJson(json); 140 | } 141 | 142 | @JsonSerializable() 143 | class HomeDataResponse { 144 | @JsonKey(name: 'services') 145 | List? services; 146 | @JsonKey(name: 'stores') 147 | List? stores; 148 | @JsonKey(name: 'banners') 149 | List? banners; 150 | 151 | HomeDataResponse(this.services, this.stores, this.banners); 152 | 153 | // toJson 154 | Map toJson() => _$HomeDataResponseToJson(this); 155 | 156 | //fromJson 157 | factory HomeDataResponse.fromJson(Map json) => 158 | _$HomeDataResponseFromJson(json); 159 | } 160 | 161 | @JsonSerializable() 162 | class HomeResponse extends BaseResponse { 163 | @JsonKey(name: 'data') 164 | HomeDataResponse? data; 165 | 166 | HomeResponse(this.data); 167 | 168 | // toJson 169 | Map toJson() => _$HomeResponseToJson(this); 170 | 171 | //fromJson 172 | factory HomeResponse.fromJson(Map json) => 173 | _$HomeResponseFromJson(json); 174 | } 175 | 176 | @JsonSerializable() 177 | class StoreDetailsResponse extends BaseResponse{ 178 | @JsonKey(name: 'id') 179 | int? id; 180 | @JsonKey(name: 'title') 181 | String? title; 182 | @JsonKey(name: 'image') 183 | String? image; 184 | @JsonKey(name: 'details') 185 | String? details; 186 | @JsonKey(name: 'services') 187 | String? services; 188 | @JsonKey(name: 'about') 189 | String? about; 190 | 191 | StoreDetailsResponse(this.id, this.title, this.image,this.details, this.services, this.about); 192 | 193 | factory StoreDetailsResponse.fromJson(Map json) => 194 | _$StoreDetailsResponseFromJson(json); 195 | 196 | Map toJson() => _$StoreDetailsResponseToJson(this); 197 | } 198 | -------------------------------------------------------------------------------- /lib/data/responses/responses.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'responses.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | BaseResponse _$BaseResponseFromJson(Map json) { 10 | return BaseResponse() 11 | ..status = json['status'] as int? 12 | ..message = json['message'] as String?; 13 | } 14 | 15 | Map _$BaseResponseToJson(BaseResponse instance) => 16 | { 17 | 'status': instance.status, 18 | 'message': instance.message, 19 | }; 20 | 21 | CustomerResponse _$CustomerResponseFromJson(Map json) { 22 | return CustomerResponse( 23 | json['id'] as String?, 24 | json['name'] as String?, 25 | json['numOfNotifications'] as int?, 26 | ); 27 | } 28 | 29 | Map _$CustomerResponseToJson(CustomerResponse instance) => 30 | { 31 | 'id': instance.id, 32 | 'name': instance.name, 33 | 'numOfNotifications': instance.numOfNotifications, 34 | }; 35 | 36 | ContactsResponse _$ContactsResponseFromJson(Map json) { 37 | return ContactsResponse( 38 | json['email'] as String?, 39 | json['phone'] as String?, 40 | json['link'] as String?, 41 | ); 42 | } 43 | 44 | Map _$ContactsResponseToJson(ContactsResponse instance) => 45 | { 46 | 'email': instance.email, 47 | 'phone': instance.phone, 48 | 'link': instance.link, 49 | }; 50 | 51 | AuthenticationResponse _$AuthenticationResponseFromJson( 52 | Map json) { 53 | return AuthenticationResponse( 54 | json['customer'] == null 55 | ? null 56 | : CustomerResponse.fromJson(json['customer'] as Map), 57 | json['contacts'] == null 58 | ? null 59 | : ContactsResponse.fromJson(json['contacts'] as Map), 60 | ) 61 | ..status = json['status'] as int? 62 | ..message = json['message'] as String?; 63 | } 64 | 65 | Map _$AuthenticationResponseToJson( 66 | AuthenticationResponse instance) => 67 | { 68 | 'status': instance.status, 69 | 'message': instance.message, 70 | 'customer': instance.customer, 71 | 'contacts': instance.contacts, 72 | }; 73 | 74 | ForgotPasswordResponse _$ForgotPasswordResponseFromJson( 75 | Map json) { 76 | return ForgotPasswordResponse( 77 | json['support'] as String?, 78 | ) 79 | ..status = json['status'] as int? 80 | ..message = json['message'] as String?; 81 | } 82 | 83 | Map _$ForgotPasswordResponseToJson( 84 | ForgotPasswordResponse instance) => 85 | { 86 | 'status': instance.status, 87 | 'message': instance.message, 88 | 'support': instance.support, 89 | }; 90 | 91 | ServiceResponse _$ServiceResponseFromJson(Map json) { 92 | return ServiceResponse( 93 | json['id'] as int?, 94 | json['title'] as String?, 95 | json['image'] as String?, 96 | ); 97 | } 98 | 99 | Map _$ServiceResponseToJson(ServiceResponse instance) => 100 | { 101 | 'id': instance.id, 102 | 'title': instance.title, 103 | 'image': instance.image, 104 | }; 105 | 106 | StoreResponse _$StoreResponseFromJson(Map json) { 107 | return StoreResponse( 108 | json['id'] as int?, 109 | json['title'] as String?, 110 | json['image'] as String?, 111 | ); 112 | } 113 | 114 | Map _$StoreResponseToJson(StoreResponse instance) => 115 | { 116 | 'id': instance.id, 117 | 'title': instance.title, 118 | 'image': instance.image, 119 | }; 120 | 121 | BannerResponse _$BannerResponseFromJson(Map json) { 122 | return BannerResponse( 123 | json['id'] as int?, 124 | json['title'] as String?, 125 | json['image'] as String?, 126 | json['link'] as String?, 127 | ); 128 | } 129 | 130 | Map _$BannerResponseToJson(BannerResponse instance) => 131 | { 132 | 'id': instance.id, 133 | 'title': instance.title, 134 | 'image': instance.image, 135 | 'link': instance.link, 136 | }; 137 | 138 | HomeDataResponse _$HomeDataResponseFromJson(Map json) { 139 | return HomeDataResponse( 140 | (json['services'] as List?) 141 | ?.map((e) => ServiceResponse.fromJson(e as Map)) 142 | .toList(), 143 | (json['stores'] as List?) 144 | ?.map((e) => StoreResponse.fromJson(e as Map)) 145 | .toList(), 146 | (json['banners'] as List?) 147 | ?.map((e) => BannerResponse.fromJson(e as Map)) 148 | .toList(), 149 | ); 150 | } 151 | 152 | Map _$HomeDataResponseToJson(HomeDataResponse instance) => 153 | { 154 | 'services': instance.services, 155 | 'stores': instance.stores, 156 | 'banners': instance.banners, 157 | }; 158 | 159 | HomeResponse _$HomeResponseFromJson(Map json) { 160 | return HomeResponse( 161 | json['data'] == null 162 | ? null 163 | : HomeDataResponse.fromJson(json['data'] as Map), 164 | ) 165 | ..status = json['status'] as int? 166 | ..message = json['message'] as String?; 167 | } 168 | 169 | Map _$HomeResponseToJson(HomeResponse instance) => 170 | { 171 | 'status': instance.status, 172 | 'message': instance.message, 173 | 'data': instance.data, 174 | }; 175 | 176 | StoreDetailsResponse _$StoreDetailsResponseFromJson(Map json) { 177 | return StoreDetailsResponse( 178 | json['id'] as int?, 179 | json['title'] as String?, 180 | json['image'] as String?, 181 | json['details'] as String?, 182 | json['services'] as String?, 183 | json['about'] as String?, 184 | ) 185 | ..status = json['status'] as int? 186 | ..message = json['message'] as String?; 187 | } 188 | 189 | Map _$StoreDetailsResponseToJson( 190 | StoreDetailsResponse instance) => 191 | { 192 | 'status': instance.status, 193 | 'message': instance.message, 194 | 'id': instance.id, 195 | 'title': instance.title, 196 | 'image': instance.image, 197 | 'details': instance.details, 198 | 'services': instance.services, 199 | 'about': instance.about, 200 | }; 201 | -------------------------------------------------------------------------------- /lib/domain/model/model.dart: -------------------------------------------------------------------------------- 1 | class SliderObject { 2 | String title; 3 | String subTitle; 4 | String image; 5 | 6 | SliderObject(this.title, this.subTitle, this.image); 7 | } 8 | 9 | class Customer { 10 | String id; 11 | String name; 12 | int numOfNotifications; 13 | 14 | Customer(this.id, this.name, this.numOfNotifications); 15 | } 16 | 17 | class Contacts { 18 | String email; 19 | String phone; 20 | String link; 21 | 22 | Contacts(this.email, this.phone, this.link); 23 | } 24 | 25 | class Authentication { 26 | Customer? customer; 27 | Contacts? contacts; 28 | 29 | Authentication(this.customer, this.contacts); 30 | } 31 | 32 | class DeviceInfo { 33 | String name; 34 | String identifier; 35 | String version; 36 | 37 | DeviceInfo(this.name, this.identifier, this.version); 38 | } 39 | 40 | class Service { 41 | int id; 42 | String title; 43 | String image; 44 | 45 | Service(this.id, this.title, this.image); 46 | } 47 | 48 | class Store { 49 | int id; 50 | String title; 51 | String image; 52 | 53 | Store(this.id, this.title, this.image); 54 | } 55 | 56 | class BannerAd { 57 | int id; 58 | String title; 59 | String image; 60 | String link; 61 | 62 | BannerAd(this.id, this.title, this.image, this.link); 63 | } 64 | 65 | class HomeData { 66 | List services; 67 | List stores; 68 | List banners; 69 | 70 | HomeData(this.services, this.stores, this.banners); 71 | } 72 | 73 | class HomeObject { 74 | HomeData data; 75 | 76 | HomeObject(this.data); 77 | } 78 | 79 | class StoreDetails { 80 | int id; 81 | String title; 82 | String image; 83 | String details; 84 | String services; 85 | String about; 86 | 87 | StoreDetails( 88 | this.id, this.title, this.image, this.details, this.services, this.about); 89 | } 90 | -------------------------------------------------------------------------------- /lib/domain/repository/repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/data/network/failure.dart'; 2 | import 'package:complete_advanced_flutter/data/request/request.dart'; 3 | import 'package:dartz/dartz.dart'; 4 | 5 | import '../model/model.dart'; 6 | 7 | abstract class Repository{ 8 | Future> login(LoginRequest loginRequest); 9 | Future> forgotPassword(String email); 10 | Future> register(RegisterRequest registerRequest); 11 | Future> getHome(); 12 | Future> getStoreDetails(); 13 | } -------------------------------------------------------------------------------- /lib/domain/usecase/base_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/data/network/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | 4 | abstract class BaseUseCase { 5 | Future> execute(In input); 6 | } 7 | -------------------------------------------------------------------------------- /lib/domain/usecase/forgot_password_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/data/network/failure.dart'; 2 | import 'package:complete_advanced_flutter/domain/repository/repository.dart'; 3 | import 'package:complete_advanced_flutter/domain/usecase/base_usecase.dart'; 4 | import 'package:dartz/dartz.dart'; 5 | 6 | class ForgotPasswordUseCase implements BaseUseCase { 7 | final Repository _repository; 8 | 9 | ForgotPasswordUseCase(this._repository); 10 | 11 | @override 12 | Future> execute(String input) async { 13 | return await _repository.forgotPassword(input); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/usecase/home_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/data/network/failure.dart'; 2 | import 'package:complete_advanced_flutter/domain/model/model.dart'; 3 | import 'package:complete_advanced_flutter/domain/repository/repository.dart'; 4 | import 'package:complete_advanced_flutter/domain/usecase/base_usecase.dart'; 5 | import 'package:dartz/dartz.dart'; 6 | 7 | class HomeUseCase extends BaseUseCase { 8 | Repository _repository; 9 | 10 | HomeUseCase(this._repository); 11 | 12 | @override 13 | Future> execute(void input) async { 14 | return await _repository.getHome(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/domain/usecase/login_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/app/functions.dart'; 2 | import 'package:complete_advanced_flutter/data/network/failure.dart'; 3 | import 'package:complete_advanced_flutter/data/request/request.dart'; 4 | import 'package:complete_advanced_flutter/domain/model/model.dart'; 5 | import 'package:complete_advanced_flutter/domain/repository/repository.dart'; 6 | import 'package:complete_advanced_flutter/domain/usecase/base_usecase.dart'; 7 | import 'package:dartz/dartz.dart'; 8 | 9 | class LoginUseCase implements BaseUseCase { 10 | Repository _repository; 11 | 12 | LoginUseCase(this._repository); 13 | 14 | @override 15 | Future> execute( 16 | LoginUseCaseInput input) async { 17 | DeviceInfo deviceInfo = await getDeviceDetails(); 18 | return await _repository.login(LoginRequest( 19 | input.email, input.password, deviceInfo.identifier, deviceInfo.name)); 20 | } 21 | } 22 | 23 | class LoginUseCaseInput { 24 | String email; 25 | String password; 26 | 27 | LoginUseCaseInput(this.email, this.password); 28 | } 29 | -------------------------------------------------------------------------------- /lib/domain/usecase/register_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/data/network/failure.dart'; 2 | import 'package:complete_advanced_flutter/data/request/request.dart'; 3 | import 'package:complete_advanced_flutter/domain/model/model.dart'; 4 | import 'package:complete_advanced_flutter/domain/repository/repository.dart'; 5 | import 'package:complete_advanced_flutter/domain/usecase/base_usecase.dart'; 6 | import 'package:dartz/dartz.dart'; 7 | 8 | class RegisterUseCase 9 | implements BaseUseCase { 10 | Repository _repository; 11 | 12 | RegisterUseCase(this._repository); 13 | 14 | @override 15 | Future> execute( 16 | RegisterUseCaseInput input) async { 17 | return await _repository.register(RegisterRequest( 18 | input.countryMobileCode, 19 | input.userName, 20 | input.email, 21 | input.password, 22 | input.profilePicture, 23 | input.mobileNumber)); 24 | } 25 | } 26 | 27 | class RegisterUseCaseInput { 28 | String mobileNumber; 29 | String countryMobileCode; 30 | String userName; 31 | String email; 32 | String password; 33 | String profilePicture; 34 | 35 | RegisterUseCaseInput(this.mobileNumber, this.countryMobileCode, this.userName, 36 | this.email, this.password, this.profilePicture); 37 | } 38 | -------------------------------------------------------------------------------- /lib/domain/usecase/store_details_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/data/network/failure.dart'; 2 | import 'package:complete_advanced_flutter/domain/model/model.dart'; 3 | import 'package:complete_advanced_flutter/domain/repository/repository.dart'; 4 | import 'package:dartz/dartz.dart'; 5 | 6 | import 'base_usecase.dart'; 7 | 8 | class StoreDetailsUseCase extends BaseUseCase { 9 | Repository repository; 10 | 11 | StoreDetailsUseCase(this.repository); 12 | 13 | @override 14 | Future> execute(void input) { 15 | return repository.getStoreDetails(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/app/di.dart'; 2 | import 'package:complete_advanced_flutter/presentation/resources/language_manager.dart'; 3 | import 'package:easy_localization/easy_localization.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_phoenix/flutter_phoenix.dart'; 6 | 7 | import 'app/app.dart'; 8 | 9 | void main() async { 10 | WidgetsFlutterBinding.ensureInitialized(); 11 | await EasyLocalization.ensureInitialized(); 12 | await initAppModule(); 13 | runApp(EasyLocalization( 14 | supportedLocales: [ENGLISH_LOCAL, ARABIC_LOCAL], 15 | path: ASSETS_PATH_LOCALISATIONS, 16 | child: Phoenix(child: MyApp()), 17 | )); 18 | } 19 | -------------------------------------------------------------------------------- /lib/presentation/base/baseviewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:complete_advanced_flutter/presentation/common/state_renderer/state_render_impl.dart'; 4 | import 'package:rxdart/rxdart.dart'; 5 | 6 | abstract class BaseViewModel extends BaseViewModelInputs 7 | with BaseViewModelOutputs { 8 | StreamController _inputStateStreamController = 9 | BehaviorSubject(); 10 | 11 | @override 12 | Sink get inputState => _inputStateStreamController.sink; 13 | 14 | @override 15 | Stream get outputState => 16 | _inputStateStreamController.stream.map((flowState) => flowState); 17 | 18 | @override 19 | void dispose() { 20 | _inputStateStreamController.close(); 21 | } 22 | 23 | // shared variables and functions that will be used through any view model. 24 | } 25 | 26 | abstract class BaseViewModelInputs { 27 | void start(); // will be called while init. of view model 28 | void dispose(); // will be called when viewmodel dies. 29 | 30 | Sink get inputState; 31 | } 32 | 33 | abstract class BaseViewModelOutputs { 34 | Stream get outputState; 35 | } 36 | -------------------------------------------------------------------------------- /lib/presentation/common/freezed_data_classes.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'freezed_data_classes.freezed.dart'; 4 | 5 | @freezed 6 | class LoginObject with _$LoginObject { 7 | factory LoginObject(String userName, String password) = _LoginObject; 8 | } 9 | 10 | @freezed 11 | class RegisterObject with _$RegisterObject { 12 | factory RegisterObject(String countryMobileCode, String mobileNumber, String userName, 13 | String email, String password, String profilePicture) = _RegisterObject; 14 | } 15 | -------------------------------------------------------------------------------- /lib/presentation/common/state_renderer/state_render_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/data/mapper/mapper.dart'; 2 | import 'package:complete_advanced_flutter/presentation/common/state_renderer/state_renderer.dart'; 3 | import 'package:complete_advanced_flutter/presentation/resources/strings_manager.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:easy_localization/easy_localization.dart'; 6 | 7 | abstract class FlowState { 8 | StateRendererType getStateRendererType(); 9 | 10 | String getMessage(); 11 | } 12 | 13 | // Loading State (POPUP, FULL SCREEN) 14 | 15 | class LoadingState extends FlowState { 16 | StateRendererType stateRendererType; 17 | String message; 18 | 19 | LoadingState({required this.stateRendererType, String? message}) 20 | : message = message ?? AppStrings.loading.tr(); 21 | 22 | @override 23 | String getMessage() => message; 24 | 25 | @override 26 | StateRendererType getStateRendererType() => stateRendererType; 27 | } 28 | 29 | // error state (POPUP, FULL LOADING) 30 | class ErrorState extends FlowState { 31 | StateRendererType stateRendererType; 32 | String message; 33 | 34 | ErrorState(this.stateRendererType, this.message); 35 | 36 | @override 37 | String getMessage() => message; 38 | 39 | @override 40 | StateRendererType getStateRendererType() => stateRendererType; 41 | } 42 | 43 | // CONTENT STATE 44 | 45 | class ContentState extends FlowState { 46 | ContentState(); 47 | 48 | @override 49 | String getMessage() => EMPTY; 50 | 51 | @override 52 | StateRendererType getStateRendererType() => 53 | StateRendererType.CONTENT_SCREEN_STATE; 54 | } 55 | 56 | // EMPTY STATE 57 | 58 | class EmptyState extends FlowState { 59 | String message; 60 | 61 | EmptyState(this.message); 62 | 63 | @override 64 | String getMessage() => message; 65 | 66 | @override 67 | StateRendererType getStateRendererType() => 68 | StateRendererType.EMPTY_SCREEN_STATE; 69 | } 70 | 71 | // success state 72 | class SuccessState extends FlowState { 73 | String message; 74 | 75 | SuccessState(this.message); 76 | 77 | @override 78 | String getMessage() => message; 79 | 80 | @override 81 | StateRendererType getStateRendererType() => StateRendererType.POPUP_SUCCESS; 82 | } 83 | 84 | extension FlowStateExtension on FlowState { 85 | Widget getScreenWidget(BuildContext context, Widget contentScreenWidget, 86 | Function retryActionFunction) { 87 | switch (this.runtimeType) { 88 | case LoadingState: 89 | { 90 | if (getStateRendererType() == StateRendererType.POPUP_LOADING_STATE) { 91 | // showing popup dialog 92 | showPopUp(context, getStateRendererType(), getMessage()); 93 | // return the content ui of the screen 94 | return contentScreenWidget; 95 | } else // StateRendererType.FULL_SCREEN_LOADING_STATE 96 | { 97 | return StateRenderer( 98 | stateRendererType: getStateRendererType(), 99 | message: getMessage(), 100 | retryActionFunction: retryActionFunction); 101 | } 102 | } 103 | case ErrorState: 104 | { 105 | dismissDialog(context); 106 | if (getStateRendererType() == StateRendererType.POPUP_ERROR_STATE) { 107 | // showing popup dialog 108 | showPopUp(context, getStateRendererType(), getMessage()); 109 | // return the content ui of the screen 110 | return contentScreenWidget; 111 | } else // StateRendererType.FULL_SCREEN_ERROR_STATE 112 | { 113 | return StateRenderer( 114 | stateRendererType: getStateRendererType(), 115 | message: getMessage(), 116 | retryActionFunction: retryActionFunction); 117 | } 118 | } 119 | case ContentState: 120 | { 121 | dismissDialog(context); 122 | return contentScreenWidget; 123 | } 124 | case EmptyState: 125 | { 126 | return StateRenderer( 127 | stateRendererType: getStateRendererType(), 128 | message: getMessage(), 129 | retryActionFunction: retryActionFunction); 130 | } 131 | case SuccessState: 132 | { 133 | // i should check if we are showing loading popup to remove it before showing success popup 134 | dismissDialog(context); 135 | 136 | // show popup 137 | showPopUp(context, StateRendererType.POPUP_SUCCESS, getMessage(), 138 | title: AppStrings.success.tr()); 139 | // return content ui of the screen 140 | return contentScreenWidget; 141 | } 142 | default: 143 | { 144 | return contentScreenWidget; 145 | } 146 | } 147 | } 148 | 149 | dismissDialog(BuildContext context) { 150 | if (_isThereCurrentDialogShowing(context)) { 151 | Navigator.of(context, rootNavigator: true).pop(true); 152 | } 153 | } 154 | 155 | _isThereCurrentDialogShowing(BuildContext context) => 156 | ModalRoute.of(context)?.isCurrent != true; 157 | 158 | showPopUp(BuildContext context, StateRendererType stateRendererType, 159 | String message,{String title = EMPTY}) { 160 | WidgetsBinding.instance?.addPostFrameCallback((_) => showDialog( 161 | context: context, 162 | builder: (BuildContext context) => StateRenderer( 163 | stateRendererType: stateRendererType, 164 | message: message, 165 | title: title, 166 | retryActionFunction: () {}, 167 | ))); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /lib/presentation/common/state_renderer/state_renderer.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/data/mapper/mapper.dart'; 2 | import 'package:complete_advanced_flutter/presentation/resources/assets_manager.dart'; 3 | import 'package:complete_advanced_flutter/presentation/resources/color_manager.dart'; 4 | import 'package:complete_advanced_flutter/presentation/resources/font_manager.dart'; 5 | import 'package:complete_advanced_flutter/presentation/resources/strings_manager.dart'; 6 | import 'package:complete_advanced_flutter/presentation/resources/styles_manager.dart'; 7 | import 'package:complete_advanced_flutter/presentation/resources/values_manager.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:lottie/lottie.dart'; 10 | import 'package:easy_localization/easy_localization.dart'; 11 | 12 | enum StateRendererType { 13 | // POPUP STATES 14 | POPUP_LOADING_STATE, 15 | POPUP_ERROR_STATE, 16 | POPUP_SUCCESS, 17 | // FULL SCREEN STATES 18 | FULL_SCREEN_LOADING_STATE, 19 | FULL_SCREEN_ERROR_STATE, 20 | CONTENT_SCREEN_STATE, // THE UI OF THE SCREEN 21 | EMPTY_SCREEN_STATE // EMPTY VIEW WHEN WE RECEIVE NO DATA FROM API SIDE FOR LIST SCREEN 22 | } 23 | 24 | class StateRenderer extends StatelessWidget { 25 | StateRendererType stateRendererType; 26 | String message; 27 | String title; 28 | Function? retryActionFunction; 29 | 30 | StateRenderer( 31 | {Key? key, 32 | required this.stateRendererType, 33 | String? message, 34 | String? title, 35 | required this.retryActionFunction}) 36 | : message = message ?? AppStrings.loading.tr(), 37 | title = title ?? EMPTY, 38 | super(key: key); 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return _getStateWidget(context); 43 | } 44 | 45 | Widget _getStateWidget(BuildContext context) { 46 | switch (stateRendererType) { 47 | case StateRendererType.POPUP_LOADING_STATE: 48 | return _getPopUpDialog( 49 | context, [_getAnimatedImage(JsonAssets.loading)]); 50 | case StateRendererType.POPUP_ERROR_STATE: 51 | return _getPopUpDialog(context, [ 52 | _getAnimatedImage(JsonAssets.error), 53 | _getMessage(message), 54 | _getRetryButton(AppStrings.ok.tr(), context) 55 | ]); 56 | case StateRendererType.POPUP_SUCCESS: 57 | return _getPopUpDialog(context, [ 58 | _getAnimatedImage(JsonAssets.success), 59 | _getMessage(title), 60 | _getMessage(message), 61 | _getRetryButton(AppStrings.ok.tr(), context) 62 | ]); 63 | case StateRendererType.FULL_SCREEN_LOADING_STATE: 64 | return _getItemsInColumn( 65 | [_getAnimatedImage(JsonAssets.loading), _getMessage(message)]); 66 | case StateRendererType.FULL_SCREEN_ERROR_STATE: 67 | return _getItemsInColumn([ 68 | _getAnimatedImage(JsonAssets.error), 69 | _getMessage(message), 70 | _getRetryButton(AppStrings.retry_again.tr(), context) 71 | ]); 72 | case StateRendererType.CONTENT_SCREEN_STATE: 73 | return Container(); 74 | case StateRendererType.EMPTY_SCREEN_STATE: 75 | return _getItemsInColumn( 76 | [_getAnimatedImage(JsonAssets.empty), _getMessage(message)]); 77 | default: 78 | return Container(); 79 | } 80 | } 81 | 82 | Widget _getPopUpDialog(BuildContext context, List children) { 83 | return Dialog( 84 | shape: RoundedRectangleBorder( 85 | borderRadius: BorderRadius.circular(AppSize.s14)), 86 | elevation: AppSize.s1_5, 87 | backgroundColor: Colors.transparent, 88 | child: Container( 89 | decoration: BoxDecoration( 90 | color: ColorManager.white, 91 | shape: BoxShape.rectangle, 92 | borderRadius: BorderRadius.circular(AppSize.s14), 93 | boxShadow: [ 94 | BoxShadow( 95 | color: Colors.black26, 96 | blurRadius: AppSize.s12, 97 | offset: Offset(AppSize.s0, AppSize.s12)) 98 | ]), 99 | child: _getDialogContent(context, children), 100 | ), 101 | ); 102 | } 103 | 104 | Widget _getDialogContent(BuildContext context, List children) { 105 | return Column( 106 | mainAxisSize: MainAxisSize.min, 107 | mainAxisAlignment: MainAxisAlignment.center, 108 | crossAxisAlignment: CrossAxisAlignment.center, 109 | children: children, 110 | ); 111 | } 112 | 113 | Widget _getAnimatedImage(String animationName) { 114 | return SizedBox( 115 | height: AppSize.s100, 116 | width: AppSize.s100, 117 | child: Lottie.asset(animationName), 118 | ); 119 | } 120 | 121 | Widget _getMessage(String message) { 122 | return Center( 123 | child: Padding( 124 | padding: const EdgeInsets.all(AppPadding.p18), 125 | child: Text( 126 | message, 127 | style: 128 | getMediumStyle(color: ColorManager.black, fontSize: FontSize.s16), 129 | ), 130 | ), 131 | ); 132 | } 133 | 134 | Widget _getRetryButton(String buttonTitle, BuildContext context) { 135 | return Center( 136 | child: Padding( 137 | padding: const EdgeInsets.all(AppPadding.p18), 138 | child: SizedBox( 139 | width: AppSize.s180, 140 | child: ElevatedButton( 141 | onPressed: () { 142 | if (stateRendererType == 143 | StateRendererType.FULL_SCREEN_ERROR_STATE) { 144 | retryActionFunction 145 | ?.call(); // to call the API function again to retry 146 | } else { 147 | Navigator.of(context) 148 | .pop(); // popup state error so we need to dismiss the dialog 149 | } 150 | }, 151 | child: Text(buttonTitle)), 152 | ), 153 | ), 154 | ); 155 | } 156 | 157 | Widget _getItemsInColumn(List children) { 158 | return Center( 159 | child: Column( 160 | mainAxisAlignment: MainAxisAlignment.center, 161 | crossAxisAlignment: CrossAxisAlignment.center, 162 | children: children, 163 | ), 164 | ); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /lib/presentation/forgot_password/forgot_password.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/app/di.dart'; 2 | import 'package:complete_advanced_flutter/presentation/common/state_renderer/state_render_impl.dart'; 3 | import 'package:complete_advanced_flutter/presentation/resources/assets_manager.dart'; 4 | import 'package:complete_advanced_flutter/presentation/resources/color_manager.dart'; 5 | import 'package:complete_advanced_flutter/presentation/resources/strings_manager.dart'; 6 | import 'package:complete_advanced_flutter/presentation/resources/values_manager.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:easy_localization/easy_localization.dart'; 9 | 10 | import 'forgot_password_viewmodel.dart'; 11 | 12 | class ForgotPasswordView extends StatefulWidget { 13 | const ForgotPasswordView({Key? key}) : super(key: key); 14 | 15 | @override 16 | _ForgotPasswordViewState createState() => _ForgotPasswordViewState(); 17 | } 18 | 19 | class _ForgotPasswordViewState extends State { 20 | final _formKey = GlobalKey(); 21 | final TextEditingController _emailTextEditingController = 22 | TextEditingController(); 23 | 24 | final ForgotPasswordViewModel _viewModel = 25 | instance(); 26 | 27 | bind() { 28 | _viewModel.start(); 29 | _emailTextEditingController.addListener( 30 | () => _viewModel.setEmail(_emailTextEditingController.text)); 31 | } 32 | 33 | @override 34 | void initState() { 35 | bind(); 36 | super.initState(); 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return Scaffold( 42 | body: StreamBuilder( 43 | stream: _viewModel.outputState, 44 | builder: (context, snapshot) { 45 | return snapshot.data?.getScreenWidget(context,_getContentWidget(), 46 | () { 47 | _viewModel.forgotPassword(); 48 | }) ?? 49 | _getContentWidget(); 50 | }, 51 | ), 52 | ); 53 | } 54 | 55 | Widget _getContentWidget() { 56 | return Container( 57 | constraints: const BoxConstraints.expand(), 58 | padding: const EdgeInsets.only(top: AppPadding.p100), 59 | color: ColorManager.white, 60 | child: SingleChildScrollView( 61 | child: Form( 62 | key: _formKey, 63 | child: Column( 64 | children: [ 65 | const Image(image: AssetImage(ImageAssets.splashLogo)), 66 | const SizedBox( 67 | height: AppSize.s28, 68 | ), 69 | Padding( 70 | padding: const EdgeInsets.only( 71 | left: AppPadding.p28, right: AppPadding.p28), 72 | child: StreamBuilder( 73 | stream: _viewModel.outputIsEmailValid, 74 | builder: (context, snapshot) { 75 | return TextFormField( 76 | keyboardType: TextInputType.emailAddress, 77 | controller: _emailTextEditingController, 78 | decoration: InputDecoration( 79 | hintText: AppStrings.emailHint.tr(), 80 | labelText: AppStrings.emailHint.tr(), 81 | errorText: (snapshot.data ?? true) 82 | ? null 83 | : AppStrings.invalidEmail.tr()), 84 | ); 85 | }, 86 | ), 87 | ), 88 | const SizedBox( 89 | height: AppSize.s28, 90 | ), 91 | Padding( 92 | padding: const EdgeInsets.only( 93 | left: AppPadding.p28, right: AppPadding.p28), 94 | child: StreamBuilder( 95 | stream: _viewModel.outputIsAllInputValid, 96 | builder: (context, snapshot) { 97 | return SizedBox( 98 | width: double.infinity, 99 | height: AppSize.s40, 100 | child: ElevatedButton( 101 | onPressed: (snapshot.data ?? false) 102 | ? () => _viewModel.forgotPassword() 103 | : null, 104 | child: const Text(AppStrings.resetPassword).tr()), 105 | ); 106 | }, 107 | ), 108 | ) 109 | ], 110 | ), 111 | ), 112 | ), 113 | ); 114 | } 115 | } -------------------------------------------------------------------------------- /lib/presentation/forgot_password/forgot_password_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:complete_advanced_flutter/app/functions.dart'; 4 | import 'package:complete_advanced_flutter/domain/usecase/forgot_password_usecase.dart'; 5 | import 'package:complete_advanced_flutter/presentation/base/baseviewmodel.dart'; 6 | import 'package:complete_advanced_flutter/presentation/common/state_renderer/state_render_impl.dart'; 7 | import 'package:complete_advanced_flutter/presentation/common/state_renderer/state_renderer.dart'; 8 | 9 | class ForgotPasswordViewModel extends BaseViewModel 10 | with ForgotPasswordViewModelInput, ForgotPasswordViewModelOutput { 11 | final StreamController _emailStreamController = 12 | StreamController.broadcast(); 13 | final StreamController _isAllInputValidStreamController = 14 | StreamController.broadcast(); 15 | 16 | final ForgotPasswordUseCase _forgotPasswordUseCase; 17 | 18 | ForgotPasswordViewModel(this._forgotPasswordUseCase); 19 | 20 | var email = ""; 21 | 22 | // input 23 | @override 24 | void start() { 25 | inputState.add(ContentState()); 26 | } 27 | 28 | @override 29 | forgotPassword() async { 30 | inputState.add( 31 | LoadingState(stateRendererType: StateRendererType.POPUP_LOADING_STATE)); 32 | (await _forgotPasswordUseCase.execute(email)).fold((failure) { 33 | inputState.add( 34 | ErrorState(StateRendererType.POPUP_ERROR_STATE, failure.message)); 35 | }, (supportMessage) { 36 | inputState.add(SuccessState(supportMessage)); 37 | }); 38 | } 39 | 40 | @override 41 | setEmail(String email) { 42 | inputEmail.add(email); 43 | this.email = email; 44 | _validate(); 45 | } 46 | 47 | @override 48 | Sink get inputEmail => _emailStreamController.sink; 49 | 50 | @override 51 | Sink get inputIsAllInputValid => _isAllInputValidStreamController.sink; 52 | 53 | // output 54 | @override 55 | void dispose() { 56 | _emailStreamController.close(); 57 | _isAllInputValidStreamController.close(); 58 | } 59 | 60 | @override 61 | Stream get outputIsEmailValid => 62 | _emailStreamController.stream.map((email) => isEmailValid(email)); 63 | 64 | @override 65 | Stream get outputIsAllInputValid => 66 | _isAllInputValidStreamController.stream 67 | .map((isAllInputValid) => _isAllInputValid()); 68 | 69 | _isAllInputValid() { 70 | return isEmailValid(email); 71 | } 72 | 73 | _validate() { 74 | inputIsAllInputValid.add(null); 75 | } 76 | } 77 | 78 | abstract class ForgotPasswordViewModelInput { 79 | forgotPassword(); 80 | 81 | setEmail(String email); 82 | 83 | Sink get inputEmail; 84 | 85 | Sink get inputIsAllInputValid; 86 | } 87 | 88 | abstract class ForgotPasswordViewModelOutput { 89 | Stream get outputIsEmailValid; 90 | 91 | Stream get outputIsAllInputValid; 92 | } 93 | -------------------------------------------------------------------------------- /lib/presentation/login/login_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:complete_advanced_flutter/domain/usecase/login_usecase.dart'; 4 | import 'package:complete_advanced_flutter/presentation/base/baseviewmodel.dart'; 5 | import 'package:complete_advanced_flutter/presentation/common/freezed_data_classes.dart'; 6 | import 'package:complete_advanced_flutter/presentation/common/state_renderer/state_render_impl.dart'; 7 | import 'package:complete_advanced_flutter/presentation/common/state_renderer/state_renderer.dart'; 8 | 9 | class LoginViewModel extends BaseViewModel 10 | with LoginViewModelInputs, LoginViewModelOutputs { 11 | StreamController _userNameStreamController = 12 | StreamController.broadcast(); 13 | StreamController _passwordStreamController = 14 | StreamController.broadcast(); 15 | 16 | StreamController _isAllInputsValidStreamController = 17 | StreamController.broadcast(); 18 | 19 | StreamController isUserLoggedInSuccessfullyStreamController = StreamController(); 20 | 21 | var loginObject = LoginObject("", ""); 22 | 23 | LoginUseCase _loginUseCase; 24 | 25 | LoginViewModel(this._loginUseCase); 26 | 27 | // inputs 28 | @override 29 | void dispose() { 30 | _userNameStreamController.close(); 31 | _isAllInputsValidStreamController.close(); 32 | _passwordStreamController.close(); 33 | isUserLoggedInSuccessfullyStreamController.close(); 34 | } 35 | 36 | @override 37 | void start() { 38 | // view tells state renderer, please show the content of the screen 39 | inputState.add(ContentState()); 40 | } 41 | 42 | @override 43 | Sink get inputPassword => _passwordStreamController.sink; 44 | 45 | @override 46 | Sink get inputUserName => _userNameStreamController.sink; 47 | 48 | @override 49 | Sink get inputIsAllInputValid => _isAllInputsValidStreamController.sink; 50 | 51 | @override 52 | login() async { 53 | inputState.add( 54 | LoadingState(stateRendererType: StateRendererType.POPUP_LOADING_STATE)); 55 | (await _loginUseCase.execute( 56 | LoginUseCaseInput(loginObject.userName, loginObject.password))) 57 | .fold( 58 | (failure) => 59 | { 60 | // left -> failure 61 | inputState.add(ErrorState( 62 | StateRendererType.POPUP_ERROR_STATE, failure.message)) 63 | }, 64 | (data) { 65 | // right -> success (data) 66 | inputState.add(ContentState()); 67 | 68 | // navigate to main screen after the login 69 | isUserLoggedInSuccessfullyStreamController.add("abcdefgh"); 70 | }); 71 | } 72 | 73 | @override 74 | setPassword(String password) { 75 | inputPassword.add(password); 76 | loginObject = loginObject.copyWith( 77 | password: password); // data class operation same as kotlin 78 | _validate(); 79 | } 80 | 81 | @override 82 | setUserName(String userName) { 83 | inputUserName.add(userName); 84 | loginObject = loginObject.copyWith( 85 | userName: userName); // data class operation same as kotlin 86 | _validate(); 87 | } 88 | 89 | // outputs 90 | @override 91 | Stream get outputIsPasswordValid => 92 | _passwordStreamController.stream 93 | .map((password) => _isPasswordValid(password)); 94 | 95 | @override 96 | Stream get outputIsUserNameValid => 97 | _userNameStreamController.stream 98 | .map((userName) => _isUserNameValid(userName)); 99 | 100 | @override 101 | Stream get outputIsAllInputsValid => 102 | _isAllInputsValidStreamController.stream.map((_) => _isAllInputsValid()); 103 | 104 | // private functions 105 | 106 | _validate() { 107 | inputIsAllInputValid.add(null); 108 | } 109 | 110 | bool _isPasswordValid(String password) { 111 | return password.isNotEmpty; 112 | } 113 | 114 | bool _isUserNameValid(String userName) { 115 | return userName.isNotEmpty; 116 | } 117 | 118 | bool _isAllInputsValid() { 119 | return _isPasswordValid(loginObject.password) && 120 | _isUserNameValid(loginObject.userName); 121 | } 122 | } 123 | 124 | abstract class LoginViewModelInputs { 125 | // three functions for actions 126 | setUserName(String userName); 127 | 128 | setPassword(String password); 129 | 130 | login(); 131 | 132 | // two sinks for streams 133 | Sink get inputUserName; 134 | 135 | Sink get inputPassword; 136 | 137 | Sink get inputIsAllInputValid; 138 | } 139 | 140 | abstract class LoginViewModelOutputs { 141 | Stream get outputIsUserNameValid; 142 | 143 | Stream get outputIsPasswordValid; 144 | 145 | Stream get outputIsAllInputsValid; 146 | } 147 | -------------------------------------------------------------------------------- /lib/presentation/main/home/home_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ffi'; 3 | 4 | import 'package:complete_advanced_flutter/domain/model/model.dart'; 5 | import 'package:complete_advanced_flutter/domain/usecase/home_usecase.dart'; 6 | import 'package:complete_advanced_flutter/presentation/base/baseviewmodel.dart'; 7 | import 'package:complete_advanced_flutter/presentation/common/state_renderer/state_render_impl.dart'; 8 | import 'package:complete_advanced_flutter/presentation/common/state_renderer/state_renderer.dart'; 9 | import 'package:rxdart/rxdart.dart'; 10 | 11 | class HomeViewModel extends BaseViewModel 12 | with HomeViewModelInputs, HomeViewModelOutputs { 13 | HomeUseCase _homeUseCase; 14 | 15 | final _dataStreamController = BehaviorSubject(); 16 | 17 | HomeViewModel(this._homeUseCase); 18 | 19 | // inputs 20 | @override 21 | void start() { 22 | _getHome(); 23 | } 24 | 25 | _getHome() async { 26 | inputState.add(LoadingState( 27 | stateRendererType: StateRendererType.FULL_SCREEN_LOADING_STATE)); 28 | 29 | (await _homeUseCase.execute(Void)).fold((failure) { 30 | inputState.add(ErrorState( 31 | StateRendererType.FULL_SCREEN_ERROR_STATE, failure.message)); 32 | }, (homeObject) { 33 | inputState.add(ContentState()); 34 | inputHomeData.add(HomeViewObject(homeObject.data.stores, 35 | homeObject.data.services, homeObject.data.banners)); 36 | }); 37 | } 38 | 39 | @override 40 | void dispose() { 41 | _dataStreamController.close(); 42 | super.dispose(); 43 | } 44 | 45 | @override 46 | Sink get inputHomeData => _dataStreamController.sink; 47 | 48 | // outputs 49 | @override 50 | Stream get outputHomeData => 51 | _dataStreamController.stream.map((data) => data); 52 | } 53 | 54 | abstract class HomeViewModelInputs { 55 | Sink get inputHomeData; 56 | } 57 | 58 | abstract class HomeViewModelOutputs { 59 | Stream get outputHomeData; 60 | } 61 | 62 | class HomeViewObject { 63 | List stores; 64 | List services; 65 | List banners; 66 | 67 | HomeViewObject(this.stores, this.services, this.banners); 68 | } 69 | -------------------------------------------------------------------------------- /lib/presentation/main/main_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/presentation/main/home/home_page.dart'; 2 | import 'package:complete_advanced_flutter/presentation/main/notifications_page.dart'; 3 | import 'package:complete_advanced_flutter/presentation/main/search_page.dart'; 4 | import 'package:complete_advanced_flutter/presentation/main/settings_page.dart'; 5 | import 'package:complete_advanced_flutter/presentation/resources/color_manager.dart'; 6 | import 'package:complete_advanced_flutter/presentation/resources/strings_manager.dart'; 7 | import 'package:complete_advanced_flutter/presentation/resources/values_manager.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:easy_localization/easy_localization.dart'; 10 | 11 | class MainView extends StatefulWidget { 12 | const MainView({Key? key}) : super(key: key); 13 | 14 | @override 15 | _MainViewState createState() => _MainViewState(); 16 | } 17 | 18 | class _MainViewState extends State { 19 | List pages = [ 20 | HomePage(), 21 | SearchPage(), 22 | NotificationsPage(), 23 | SettingsPage() 24 | ]; 25 | List titles = [ 26 | AppStrings.home.tr(), 27 | AppStrings.search.tr(), 28 | AppStrings.notifications.tr(), 29 | AppStrings.settings.tr(), 30 | ]; 31 | var _title = AppStrings.home.tr(); 32 | var _currentIndex = 0; 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return Scaffold( 37 | appBar: AppBar( 38 | title: Text( 39 | _title, 40 | style: Theme.of(context).textTheme.headline2, 41 | ), 42 | ), 43 | body: pages[_currentIndex], 44 | bottomNavigationBar: Container( 45 | decoration: BoxDecoration(boxShadow: [ 46 | BoxShadow(color: ColorManager.lightGrey, spreadRadius: AppSize.s1) 47 | ]), 48 | child: BottomNavigationBar( 49 | selectedItemColor: ColorManager.primary, 50 | unselectedItemColor: ColorManager.grey, 51 | currentIndex: _currentIndex, 52 | onTap: onTap, 53 | items: [ 54 | BottomNavigationBarItem( 55 | icon: Icon(Icons.home), label: AppStrings.home.tr()), 56 | BottomNavigationBarItem( 57 | icon: Icon(Icons.search), label: AppStrings.search.tr()), 58 | BottomNavigationBarItem( 59 | icon: Icon(Icons.notifications), 60 | label: AppStrings.notifications.tr()), 61 | BottomNavigationBarItem( 62 | icon: Icon(Icons.settings), label: AppStrings.settings.tr()), 63 | ], 64 | ), 65 | ), 66 | ); 67 | } 68 | 69 | onTap(int index) { 70 | setState(() { 71 | _currentIndex = index; 72 | _title = titles[index]; 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/presentation/main/notifications_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/presentation/resources/strings_manager.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:easy_localization/easy_localization.dart'; 4 | 5 | class NotificationsPage extends StatefulWidget { 6 | const NotificationsPage({Key? key}) : super(key: key); 7 | 8 | @override 9 | _NotificationsPageState createState() => _NotificationsPageState(); 10 | } 11 | 12 | class _NotificationsPageState extends State { 13 | @override 14 | Widget build(BuildContext context) { 15 | return Center( 16 | child: Text(AppStrings.notifications).tr(), 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/presentation/main/search_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/presentation/resources/strings_manager.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:easy_localization/easy_localization.dart'; 4 | class SearchPage extends StatefulWidget { 5 | const SearchPage({Key? key}) : super(key: key); 6 | 7 | @override 8 | _SearchPageState createState() => _SearchPageState(); 9 | } 10 | 11 | class _SearchPageState extends State { 12 | @override 13 | Widget build(BuildContext context) { 14 | return Center( 15 | child: Text(AppStrings.search).tr(), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/presentation/main/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/app/app_prefs.dart'; 2 | import 'package:complete_advanced_flutter/app/di.dart'; 3 | import 'package:complete_advanced_flutter/data/data_source/local_data_source.dart'; 4 | import 'package:complete_advanced_flutter/presentation/resources/assets_manager.dart'; 5 | import 'package:complete_advanced_flutter/presentation/resources/language_manager.dart'; 6 | import 'package:complete_advanced_flutter/presentation/resources/routes_manager.dart'; 7 | import 'package:complete_advanced_flutter/presentation/resources/strings_manager.dart'; 8 | import 'package:complete_advanced_flutter/presentation/resources/values_manager.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_phoenix/flutter_phoenix.dart'; 11 | import 'package:flutter_svg/svg.dart'; 12 | import 'package:easy_localization/easy_localization.dart'; 13 | 14 | import 'dart:math' as math; 15 | 16 | class SettingsPage extends StatefulWidget { 17 | const SettingsPage({Key? key}) : super(key: key); 18 | 19 | @override 20 | _SettingsPageState createState() => _SettingsPageState(); 21 | } 22 | 23 | class _SettingsPageState extends State { 24 | AppPreferences _appPreferences = instance(); 25 | LocalDataSource _localDataSource = instance(); 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return ListView( 30 | padding: EdgeInsets.all(AppPadding.p8), 31 | children: [ 32 | ListTile( 33 | title: Text( 34 | AppStrings.changeLanguage, 35 | style: Theme.of(context).textTheme.headline4, 36 | ).tr(), 37 | leading: SvgPicture.asset(ImageAssets.changeLangIc), 38 | trailing: Transform( 39 | alignment: Alignment.center, 40 | transform: Matrix4.rotationY(isRtl() ? math.pi : 0), 41 | child: SvgPicture.asset(ImageAssets.settingsRightArrowIc), 42 | ), 43 | onTap: () { 44 | _changeLanguage(); 45 | }, 46 | ), 47 | ListTile( 48 | title: Text( 49 | AppStrings.contactUs, 50 | style: Theme.of(context).textTheme.headline4, 51 | ).tr(), 52 | leading: SvgPicture.asset(ImageAssets.contactUsIc), 53 | trailing: Transform( 54 | alignment: Alignment.center, 55 | transform: Matrix4.rotationY(isRtl() ? math.pi : 0), 56 | child: SvgPicture.asset(ImageAssets.settingsRightArrowIc), 57 | ), 58 | onTap: () { 59 | _contactUs(); 60 | }, 61 | ), 62 | ListTile( 63 | title: Text( 64 | AppStrings.inviteYourFriends, 65 | style: Theme.of(context).textTheme.headline4, 66 | ).tr(), 67 | leading: SvgPicture.asset(ImageAssets.inviteFriendsIc), 68 | trailing: Transform( 69 | alignment: Alignment.center, 70 | transform: Matrix4.rotationY(isRtl() ? math.pi : 0), 71 | child: SvgPicture.asset(ImageAssets.settingsRightArrowIc), 72 | ), 73 | onTap: () { 74 | _inviteFriends(); 75 | }, 76 | ), 77 | ListTile( 78 | title: Text( 79 | AppStrings.logout, 80 | style: Theme.of(context).textTheme.headline4, 81 | ).tr(), 82 | leading: SvgPicture.asset(ImageAssets.logoutIc), 83 | trailing: Transform( 84 | alignment: Alignment.center, 85 | transform: Matrix4.rotationY(isRtl() ? math.pi : 0), 86 | child: SvgPicture.asset(ImageAssets.settingsRightArrowIc), 87 | ), 88 | onTap: () { 89 | _logout(); 90 | }, 91 | ) 92 | ], 93 | ); 94 | } 95 | 96 | bool isRtl() { 97 | return context.locale == ARABIC_LOCAL; // app is in arabic language 98 | } 99 | 100 | void _changeLanguage() { 101 | // i will apply localisation later 102 | _appPreferences.setLanguageChanged(); 103 | Phoenix.rebirth(context); // restart to apply language changes 104 | } 105 | 106 | void _contactUs() { 107 | // its a task for you to open any web bage with dummy content 108 | } 109 | 110 | void _inviteFriends() { 111 | // its a task to share app name with friends 112 | } 113 | 114 | void _logout() { 115 | _appPreferences.logout(); // clear login flag from app prefs 116 | _localDataSource.clearCache(); 117 | Navigator.pushReplacementNamed(context, Routes.loginRoute); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/presentation/onboarding/onboarding.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/app/app_prefs.dart'; 2 | import 'package:complete_advanced_flutter/app/di.dart'; 3 | import 'package:complete_advanced_flutter/domain/model/model.dart'; 4 | import 'package:complete_advanced_flutter/presentation/onboarding/onboarding_viewmodel.dart'; 5 | import 'package:complete_advanced_flutter/presentation/resources/assets_manager.dart'; 6 | import 'package:complete_advanced_flutter/presentation/resources/color_manager.dart'; 7 | import 'package:complete_advanced_flutter/presentation/resources/routes_manager.dart'; 8 | import 'package:complete_advanced_flutter/presentation/resources/strings_manager.dart'; 9 | import 'package:complete_advanced_flutter/presentation/resources/values_manager.dart'; 10 | import 'package:flutter/cupertino.dart'; 11 | import 'package:flutter/material.dart'; 12 | import 'package:flutter/services.dart'; 13 | import 'package:flutter_svg/flutter_svg.dart'; 14 | import 'package:flutter_svg/svg.dart'; 15 | import 'package:easy_localization/easy_localization.dart'; 16 | 17 | class OnBoardingView extends StatefulWidget { 18 | const OnBoardingView({Key? key}) : super(key: key); 19 | 20 | @override 21 | _OnBoardingViewState createState() => _OnBoardingViewState(); 22 | } 23 | 24 | class _OnBoardingViewState extends State { 25 | PageController _pageController = PageController(initialPage: 0); 26 | OnBoardingViewModel _viewModel = OnBoardingViewModel(); 27 | AppPreferences _appPreferences = instance(); 28 | 29 | _bind() { 30 | _appPreferences.setOnBoardingScreenViewed(); 31 | _viewModel.start(); 32 | } 33 | 34 | @override 35 | void initState() { 36 | _bind(); 37 | super.initState(); 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return StreamBuilder( 43 | stream: _viewModel.outputSliderViewObject, 44 | builder: (context, snapShot) { 45 | return _getContentWidget(snapShot.data); 46 | }); 47 | } 48 | 49 | Widget _getContentWidget(SliderViewObject? sliderViewObject) { 50 | if (sliderViewObject == null) { 51 | return Container(); 52 | } else 53 | return Scaffold( 54 | backgroundColor: ColorManager.white, 55 | appBar: AppBar( 56 | backgroundColor: ColorManager.white, 57 | elevation: AppSize.s0, 58 | systemOverlayStyle: SystemUiOverlayStyle( 59 | statusBarColor: ColorManager.white, 60 | statusBarBrightness: Brightness.dark, 61 | statusBarIconBrightness: Brightness.dark, 62 | ), 63 | ), 64 | body: PageView.builder( 65 | controller: _pageController, 66 | itemCount: sliderViewObject.numOfSlides, 67 | onPageChanged: (index) { 68 | _viewModel.onPageChanged(index); 69 | }, 70 | itemBuilder: (context, index) { 71 | return OnBoardingPage(sliderViewObject.sliderObject); 72 | }), 73 | bottomSheet: Container( 74 | color: ColorManager.white, 75 | height: AppSize.s100, 76 | child: Column( 77 | children: [ 78 | Align( 79 | alignment: Alignment.centerRight, 80 | child: TextButton( 81 | onPressed: () { 82 | Navigator.pushReplacementNamed( 83 | context, Routes.loginRoute); 84 | }, 85 | child: Text( 86 | AppStrings.skip, 87 | style: Theme.of(context).textTheme.subtitle2, 88 | textAlign: TextAlign.end, 89 | ).tr(), 90 | )), 91 | // add layout for indicator and arrows 92 | _getBottomSheetWidget(sliderViewObject) 93 | ], 94 | ), 95 | ), 96 | ); 97 | } 98 | 99 | Widget _getBottomSheetWidget(SliderViewObject sliderViewObject) { 100 | return Container( 101 | color: ColorManager.primary, 102 | child: Row( 103 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 104 | children: [ 105 | // left arrow 106 | Padding( 107 | padding: EdgeInsets.all(AppPadding.p14), 108 | child: GestureDetector( 109 | child: SizedBox( 110 | height: AppSize.s20, 111 | width: AppSize.s20, 112 | child: SvgPicture.asset(ImageAssets.leftArrowIc), 113 | ), 114 | onTap: () { 115 | // go to previous slide 116 | _pageController.animateToPage(_viewModel.goPrevious(), 117 | duration: Duration(milliseconds: DurationConstant.d300), 118 | curve: Curves.bounceInOut); 119 | }, 120 | ), 121 | ), 122 | 123 | // circles indicator 124 | Row( 125 | children: [ 126 | for (int i = 0; i < sliderViewObject.numOfSlides; i++) 127 | Padding( 128 | padding: EdgeInsets.all(AppPadding.p8), 129 | child: _getProperCircle(i, sliderViewObject.currentIndex), 130 | ) 131 | ], 132 | ), 133 | 134 | // right arrow 135 | Padding( 136 | padding: EdgeInsets.all(AppPadding.p14), 137 | child: GestureDetector( 138 | child: SizedBox( 139 | height: AppSize.s20, 140 | width: AppSize.s20, 141 | child: SvgPicture.asset(ImageAssets.rightarrowIc), 142 | ), 143 | onTap: () { 144 | // go to next slide 145 | _pageController.animateToPage(_viewModel.goNext(), 146 | duration: Duration(milliseconds: DurationConstant.d300), 147 | curve: Curves.bounceInOut); 148 | }, 149 | ), 150 | ) 151 | ], 152 | ), 153 | ); 154 | } 155 | 156 | Widget _getProperCircle(int index, int _currentIndex) { 157 | if (index == _currentIndex) { 158 | return SvgPicture.asset(ImageAssets.hollowCircleIc); // selected slider 159 | } else { 160 | return SvgPicture.asset(ImageAssets.solidCircleIc); // unselected slider 161 | } 162 | } 163 | 164 | @override 165 | void dispose() { 166 | _viewModel.dispose(); 167 | super.dispose(); 168 | } 169 | } 170 | 171 | class OnBoardingPage extends StatelessWidget { 172 | SliderObject _sliderObject; 173 | 174 | OnBoardingPage(this._sliderObject, {Key? key}) : super(key: key); 175 | 176 | @override 177 | Widget build(BuildContext context) { 178 | return Column( 179 | mainAxisAlignment: MainAxisAlignment.start, 180 | children: [ 181 | SizedBox(height: AppSize.s40), 182 | Padding( 183 | padding: const EdgeInsets.all(AppPadding.p8), 184 | child: Text( 185 | _sliderObject.title, 186 | textAlign: TextAlign.center, 187 | style: Theme.of(context).textTheme.headline1, 188 | ), 189 | ), 190 | Padding( 191 | padding: const EdgeInsets.all(AppPadding.p8), 192 | child: Text( 193 | _sliderObject.subTitle, 194 | textAlign: TextAlign.center, 195 | style: Theme.of(context).textTheme.subtitle1, 196 | ), 197 | ), 198 | SizedBox( 199 | height: AppSize.s60, 200 | ), 201 | SvgPicture.asset(_sliderObject.image) 202 | // image widget 203 | ], 204 | ); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /lib/presentation/onboarding/onboarding_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:complete_advanced_flutter/domain/model/model.dart'; 4 | import 'package:complete_advanced_flutter/presentation/base/baseviewmodel.dart'; 5 | import 'package:complete_advanced_flutter/presentation/resources/assets_manager.dart'; 6 | import 'package:complete_advanced_flutter/presentation/resources/strings_manager.dart'; 7 | import 'package:easy_localization/easy_localization.dart'; 8 | 9 | class OnBoardingViewModel extends BaseViewModel 10 | with OnBoardingViewModelInputs, OnBoardingViewModelOutputs { 11 | // stream controllers 12 | final StreamController _streamController = 13 | StreamController(); 14 | 15 | late final List _list; 16 | 17 | int _currentIndex = 0; 18 | 19 | // inputs 20 | @override 21 | void dispose() { 22 | _streamController.close(); 23 | } 24 | 25 | @override 26 | void start() { 27 | _list = _getSliderData(); 28 | // send this slider data to our view 29 | _postDataToView(); 30 | } 31 | 32 | @override 33 | int goNext() { 34 | int nextIndex = _currentIndex++; // +1 35 | if (nextIndex >= _list.length) { 36 | _currentIndex = 0; // infinite loop to go to first item inside the slider 37 | } 38 | return _currentIndex; 39 | } 40 | 41 | @override 42 | int goPrevious() { 43 | int previousIndex = _currentIndex--; // -1 44 | if (previousIndex == -1) { 45 | _currentIndex = 46 | _list.length - 1; // infinite loop to go to the length of slider list 47 | } 48 | return _currentIndex; 49 | } 50 | 51 | @override 52 | void onPageChanged(int index) { 53 | _currentIndex = index; 54 | _postDataToView(); 55 | } 56 | 57 | @override 58 | Sink get inputSliderViewObject => _streamController.sink; 59 | 60 | // outputs 61 | @override 62 | Stream get outputSliderViewObject => 63 | _streamController.stream.map((slideViewObject) => slideViewObject); 64 | 65 | // private functions 66 | List _getSliderData() => [ 67 | SliderObject( 68 | AppStrings.onBoardingTitle1.tr(), 69 | AppStrings.onBoardingSubTitle1.tr(), 70 | ImageAssets.onboardingLogo1), 71 | SliderObject( 72 | AppStrings.onBoardingTitle2.tr(), 73 | AppStrings.onBoardingSubTitle2.tr(), 74 | ImageAssets.onboardingLogo2), 75 | SliderObject( 76 | AppStrings.onBoardingTitle3.tr(), 77 | AppStrings.onBoardingSubTitle3.tr(), 78 | ImageAssets.onboardingLogo3), 79 | SliderObject( 80 | AppStrings.onBoardingTitle4.tr(), 81 | AppStrings.onBoardingSubTitle4.tr(), 82 | ImageAssets.onboardingLogo4) 83 | ]; 84 | 85 | _postDataToView() { 86 | inputSliderViewObject.add( 87 | SliderViewObject(_list[_currentIndex], _list.length, _currentIndex)); 88 | } 89 | } 90 | 91 | // inputs mean the orders that our view model will recieve from our view 92 | abstract class OnBoardingViewModelInputs { 93 | void goNext(); // when user clicks on right arrow or swipe left. 94 | void goPrevious(); // when user clicks on left arrow or swipe right. 95 | void onPageChanged(int index); 96 | 97 | Sink 98 | get inputSliderViewObject; // this is the way to add data to the stream .. stream input 99 | } 100 | 101 | // outputs mean data or results that will be sent from our view model to our view 102 | abstract class OnBoardingViewModelOutputs { 103 | Stream get outputSliderViewObject; 104 | } 105 | 106 | class SliderViewObject { 107 | SliderObject sliderObject; 108 | int numOfSlides; 109 | int currentIndex; 110 | 111 | SliderViewObject(this.sliderObject, this.numOfSlides, this.currentIndex); 112 | } 113 | -------------------------------------------------------------------------------- /lib/presentation/resources/assets_manager.dart: -------------------------------------------------------------------------------- 1 | const String IMAGE_PATH = "assets/images"; 2 | const String JSON_PATH = "assets/json"; 3 | 4 | class ImageAssets { 5 | static const String splashLogo = "$IMAGE_PATH/splash_logo.png"; 6 | static const String onboardingLogo1 = "$IMAGE_PATH/onboarding_logo1.svg"; 7 | static const String onboardingLogo2 = "$IMAGE_PATH/onboarding_logo2.svg"; 8 | static const String onboardingLogo3 = "$IMAGE_PATH/onboarding_logo3.svg"; 9 | static const String onboardingLogo4 = "$IMAGE_PATH/onboarding_logo4.svg"; 10 | static const String hollowCircleIc = "$IMAGE_PATH/hollow_cirlce_ic.svg"; 11 | static const String leftArrowIc = "$IMAGE_PATH/left_arrow_ic.svg"; 12 | static const String rightarrowIc = "$IMAGE_PATH/right_arrow_ic.svg"; 13 | static const String solidCircleIc = "$IMAGE_PATH/solid_circle_ic.svg"; 14 | static const String photoCameraIc = "$IMAGE_PATH/photo_camera_ic.svg"; 15 | static const String settingsRightArrowIc = 16 | "$IMAGE_PATH/settings_right_arrow_ic.svg"; 17 | static const String changeLangIc = "$IMAGE_PATH/change_lang_ic.svg"; 18 | static const String contactUsIc = "$IMAGE_PATH/contact_us_ic.svg"; 19 | static const String inviteFriendsIc = "$IMAGE_PATH/invite_friends_ic.svg"; 20 | static const String logoutIc = "$IMAGE_PATH/logout_ic.svg"; 21 | } 22 | 23 | class JsonAssets { 24 | static const String loading = "$JSON_PATH/loading.json"; 25 | static const String error = "$JSON_PATH/error.json"; 26 | static const String empty = "$JSON_PATH/empty.json"; 27 | static const String success = "$JSON_PATH/success.json"; 28 | } 29 | -------------------------------------------------------------------------------- /lib/presentation/resources/color_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ColorManager { 4 | static Color primary = HexColor.fromHex("#ED9728"); 5 | static Color darkGrey = HexColor.fromHex("#525252"); 6 | static Color grey = HexColor.fromHex("#737477"); 7 | static Color lightGrey = HexColor.fromHex("#9E9E9E"); 8 | static Color primaryOpacity70 = HexColor.fromHex("#B3ED9728"); 9 | 10 | // new colors 11 | static Color darkPrimary = HexColor.fromHex("#d17d11"); 12 | static Color grey1 = HexColor.fromHex("#707070"); 13 | static Color grey2 = HexColor.fromHex("#797979"); 14 | static Color white = HexColor.fromHex("#FFFFFF"); 15 | static Color error = HexColor.fromHex("#e61f34"); 16 | static Color black= HexColor.fromHex("#000000"); // red color 17 | } 18 | 19 | extension HexColor on Color { 20 | static Color fromHex(String hexColorString) { 21 | hexColorString = hexColorString.replaceAll('#', ''); 22 | if (hexColorString.length == 6) { 23 | hexColorString = "FF" + hexColorString; // 8 char with opacity 100% 24 | } 25 | return Color(int.parse(hexColorString, radix: 16)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/presentation/resources/font_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FontConstants { 4 | static const String fontFamily = "Montserrat"; 5 | } 6 | 7 | class FontWeightManager { 8 | static const FontWeight light = FontWeight.w300; 9 | static const FontWeight regular = FontWeight.w400; 10 | static const FontWeight medium = FontWeight.w500; 11 | static const FontWeight semiBold = FontWeight.w600; 12 | static const FontWeight bold = FontWeight.w700; 13 | } 14 | 15 | class FontSize { 16 | static const double s12 = 12.0; 17 | static const double s14 = 14.0; 18 | static const double s16 = 16.0; 19 | static const double s17 = 17.0; 20 | static const double s18 = 18.0; 21 | static const double s20 = 20.0; 22 | } 23 | -------------------------------------------------------------------------------- /lib/presentation/resources/language_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum LanguageType { ENGLISH, ARABIC } 4 | 5 | const String ARABIC = "ar"; 6 | const String ENGLISH = "en"; 7 | const String ASSETS_PATH_LOCALISATIONS = "assets/translations"; 8 | const Locale ARABIC_LOCAL = Locale("ar","SA"); 9 | const Locale ENGLISH_LOCAL = Locale("en","US"); 10 | 11 | extension LanguageTypeExtension on LanguageType { 12 | String getValue() { 13 | switch (this) { 14 | case LanguageType.ENGLISH: 15 | return ENGLISH; 16 | case LanguageType.ARABIC: 17 | return ARABIC; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/presentation/resources/routes_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/app/di.dart'; 2 | import 'package:complete_advanced_flutter/presentation/forgot_password/forgot_password.dart'; 3 | import 'package:complete_advanced_flutter/presentation/login/login.dart'; 4 | import 'package:complete_advanced_flutter/presentation/main/main_view.dart'; 5 | import 'package:complete_advanced_flutter/presentation/onboarding/onboarding.dart'; 6 | import 'package:complete_advanced_flutter/presentation/register/register.dart'; 7 | import 'package:complete_advanced_flutter/presentation/resources/strings_manager.dart'; 8 | import 'package:complete_advanced_flutter/presentation/splash/splash.dart'; 9 | import 'package:complete_advanced_flutter/presentation/store_details/store_details.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:easy_localization/easy_localization.dart'; 12 | 13 | class Routes { 14 | static const String splashRoute = "/"; 15 | static const String onBoardingRoute = "/onBoarding"; 16 | static const String loginRoute = "/login"; 17 | static const String registerRoute = "/register"; 18 | static const String forgotPasswordRoute = "/forgotPassword"; 19 | static const String mainRoute = "/main"; 20 | static const String storeDetailsRoute = "/storeDetails"; 21 | } 22 | 23 | class RouteGenerator { 24 | static Route getRoute(RouteSettings routeSettings) { 25 | switch (routeSettings.name) { 26 | case Routes.splashRoute: 27 | return MaterialPageRoute(builder: (_) => SplashView()); 28 | case Routes.loginRoute: 29 | initLoginModule(); 30 | return MaterialPageRoute(builder: (_) => LoginView()); 31 | case Routes.onBoardingRoute: 32 | return MaterialPageRoute(builder: (_) => OnBoardingView()); 33 | case Routes.registerRoute: 34 | initRegisterModule(); 35 | return MaterialPageRoute(builder: (_) => RegisterView()); 36 | case Routes.forgotPasswordRoute: 37 | initForgotPasswordModule(); 38 | return MaterialPageRoute(builder: (_) => ForgotPasswordView()); 39 | case Routes.mainRoute: 40 | initHomeModule(); 41 | return MaterialPageRoute(builder: (_) => MainView()); 42 | case Routes.storeDetailsRoute: 43 | initStoreDetailsModule(); 44 | return MaterialPageRoute(builder: (_) => StoreDetailsView()); 45 | default: 46 | return unDefinedRoute(); 47 | } 48 | } 49 | 50 | static Route unDefinedRoute() { 51 | return MaterialPageRoute( 52 | builder: (_) => Scaffold( 53 | appBar: AppBar( 54 | title: Text(AppStrings.noRouteFound).tr(), 55 | ), 56 | body: Center(child: Text(AppStrings.noRouteFound).tr()), 57 | )); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/presentation/resources/strings_manager.dart: -------------------------------------------------------------------------------- 1 | class AppStrings { 2 | static const String noRouteFound = "no_route_found"; 3 | static const String onBoardingTitle1 = "on_boarding_title1"; 4 | static const String onBoardingTitle2 = "on_boarding_title2"; 5 | static const String onBoardingTitle3 = "on_boarding_title3"; 6 | static const String onBoardingTitle4 = "on_boarding_title4"; 7 | 8 | static const String onBoardingSubTitle1 = "on_boarding_desc1"; 9 | static const String onBoardingSubTitle2 = "on_boarding_desc2"; 10 | static const String onBoardingSubTitle3 = "on_boarding_desc3"; 11 | static const String onBoardingSubTitle4 = "on_boarding_desc4"; 12 | static const String skip = "skip"; 13 | static const String username = "username_hint"; 14 | static const String mobileNumber = "mobile_number_hint"; 15 | static const String usernameError = "invalid_user_name"; 16 | static const String passwordError = "invalid_password"; 17 | static const String password = "password_hint"; 18 | static const String login = "login_button"; 19 | static const String forgetPassword = "forgot_password_text"; 20 | static const String registerText = "register_text"; 21 | static const String loading = "loading"; 22 | static const String retry_again = "retry_again"; 23 | static const String ok = "ok"; 24 | static const String emailHint = 'email_hint'; 25 | static const String invalidEmail = "invalid_email"; 26 | static const String resetPassword = "reset_password"; 27 | static const String success = "success"; 28 | static const String profilePicture = "upload_profile_picture"; 29 | static const String photoGalley = "photo_gallery"; 30 | static const String photoCamera = "camera"; 31 | static const String register = "register"; 32 | static const String haveAccount = "already_have_account"; 33 | static const String home = "home"; 34 | static const String notifications = "notification"; 35 | static const String search = "search"; 36 | static const String settings = "settings"; 37 | static const String services = "services"; 38 | static const String stores = "stores"; 39 | static const String details = "details"; 40 | static const String about = "about"; 41 | static const String storeDetails = "store_details"; 42 | static const String changeLanguage = "change_language"; 43 | static const String contactUs = "contact_us"; 44 | static const String inviteYourFriends = "invite_your_friends"; 45 | static const String logout = "logout"; 46 | 47 | // error handler 48 | static const String badRequestError = "bad_request_error"; 49 | static const String noContent = "no_content"; 50 | static const String forbiddenError = "forbidden_error"; 51 | static const String unauthorizedError = "unauthorized_error"; 52 | static const String notFoundError = "not_found_error"; 53 | static const String conflictError = "conflict_error"; 54 | static const String internalServerError = "internal_server_error"; 55 | static const String unknownError = "unknown_error"; 56 | static const String timeoutError = "timeout_error"; 57 | static const String defaultError = "default_error"; 58 | static const String cacheError = "cache_error"; 59 | static const String noInternetError = "no_internet_error"; 60 | } 61 | -------------------------------------------------------------------------------- /lib/presentation/resources/styles_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'font_manager.dart'; 4 | 5 | TextStyle _getTextStyle( 6 | double fontSize, String fontFamily, FontWeight fontWeight, Color color) { 7 | return TextStyle( 8 | fontSize: fontSize, 9 | fontFamily: fontFamily, 10 | color: color, 11 | fontWeight: fontWeight); 12 | } 13 | 14 | // regular style 15 | 16 | TextStyle getRegularStyle( 17 | {double fontSize = FontSize.s12, required Color color}) { 18 | return _getTextStyle( 19 | fontSize, FontConstants.fontFamily, FontWeightManager.regular, color); 20 | } 21 | // light text style 22 | 23 | TextStyle getLightStyle( 24 | {double fontSize = FontSize.s12, required Color color}) { 25 | return _getTextStyle( 26 | fontSize, FontConstants.fontFamily, FontWeightManager.light, color); 27 | } 28 | // bold text style 29 | 30 | TextStyle getBoldStyle( 31 | {double fontSize = FontSize.s12, required Color color}) { 32 | return _getTextStyle( 33 | fontSize, FontConstants.fontFamily, FontWeightManager.bold, color); 34 | } 35 | 36 | // semi bold text style 37 | 38 | TextStyle getSemiBoldStyle( 39 | {double fontSize = FontSize.s12, required Color color}) { 40 | return _getTextStyle( 41 | fontSize, FontConstants.fontFamily, FontWeightManager.semiBold, color); 42 | } 43 | 44 | 45 | // medium text style 46 | 47 | TextStyle getMediumStyle( 48 | {double fontSize = FontSize.s12, required Color color}) { 49 | return _getTextStyle( 50 | fontSize, FontConstants.fontFamily, FontWeightManager.medium, color); 51 | } 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /lib/presentation/resources/theme_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/presentation/resources/color_manager.dart'; 2 | import 'package:complete_advanced_flutter/presentation/resources/font_manager.dart'; 3 | import 'package:complete_advanced_flutter/presentation/resources/styles_manager.dart'; 4 | import 'package:complete_advanced_flutter/presentation/resources/values_manager.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | ThemeData getApplicationTheme() { 8 | return ThemeData( 9 | // main colors of the app 10 | primaryColor: ColorManager.primary, 11 | primaryColorLight: ColorManager.primaryOpacity70, 12 | primaryColorDark: ColorManager.darkPrimary, 13 | disabledColor: ColorManager.grey1, 14 | // ripple color 15 | splashColor: ColorManager.primaryOpacity70, 16 | // will be used incase of disabled button for example 17 | accentColor: ColorManager.grey, 18 | // card view theme 19 | cardTheme: CardTheme( 20 | color: ColorManager.white, 21 | shadowColor: ColorManager.grey, 22 | elevation: AppSize.s4), 23 | // App bar theme 24 | appBarTheme: AppBarTheme( 25 | centerTitle: true, 26 | color: ColorManager.primary, 27 | elevation: AppSize.s4, 28 | shadowColor: ColorManager.primaryOpacity70, 29 | titleTextStyle: getRegularStyle( 30 | color: ColorManager.white, fontSize: FontSize.s16)), 31 | // Button theme 32 | buttonTheme: ButtonThemeData( 33 | shape: StadiumBorder(), 34 | disabledColor: ColorManager.grey1, 35 | buttonColor: ColorManager.primary, 36 | splashColor: ColorManager.primaryOpacity70), 37 | 38 | // elevated button theme 39 | elevatedButtonTheme: ElevatedButtonThemeData( 40 | style: ElevatedButton.styleFrom( 41 | textStyle: getRegularStyle(color: ColorManager.white), 42 | primary: ColorManager.primary, 43 | shape: RoundedRectangleBorder( 44 | borderRadius: BorderRadius.circular(AppSize.s12)))), 45 | 46 | // Text theme 47 | textTheme: TextTheme( 48 | headline1: getSemiBoldStyle( 49 | color: ColorManager.darkGrey, fontSize: FontSize.s16), 50 | headline2: getRegularStyle( 51 | color: ColorManager.white, fontSize: FontSize.s16), 52 | headline3: 53 | getBoldStyle(color: ColorManager.primary, fontSize: FontSize.s16), 54 | headline4: getRegularStyle( 55 | color: ColorManager.primary, fontSize: FontSize.s14), 56 | subtitle1: getMediumStyle( 57 | color: ColorManager.lightGrey, fontSize: FontSize.s14), 58 | subtitle2: getMediumStyle( 59 | color: ColorManager.primary, fontSize: FontSize.s14), 60 | bodyText2: getMediumStyle(color: ColorManager.lightGrey), 61 | caption: getRegularStyle(color: ColorManager.grey1), 62 | bodyText1: getRegularStyle(color: ColorManager.grey)), 63 | // input decoration theme (text form field) 64 | 65 | inputDecorationTheme: InputDecorationTheme( 66 | contentPadding: EdgeInsets.all(AppPadding.p8), 67 | // hint style 68 | hintStyle: getRegularStyle(color: ColorManager.grey1), 69 | 70 | // label style 71 | labelStyle: getMediumStyle(color: ColorManager.darkGrey), 72 | // error style 73 | errorStyle: getRegularStyle(color: ColorManager.error), 74 | 75 | // enabled border 76 | enabledBorder: OutlineInputBorder( 77 | borderSide: 78 | BorderSide(color: ColorManager.grey, width: AppSize.s1_5), 79 | borderRadius: BorderRadius.all(Radius.circular(AppSize.s8))), 80 | 81 | // focused border 82 | focusedBorder: OutlineInputBorder( 83 | borderSide: 84 | BorderSide(color: ColorManager.primary, width: AppSize.s1_5), 85 | borderRadius: BorderRadius.all(Radius.circular(AppSize.s8))), 86 | 87 | // error border 88 | errorBorder: OutlineInputBorder( 89 | borderSide: 90 | BorderSide(color: ColorManager.error, width: AppSize.s1_5), 91 | borderRadius: BorderRadius.all(Radius.circular(AppSize.s8))), 92 | // focused error border 93 | focusedErrorBorder: OutlineInputBorder( 94 | borderSide: 95 | BorderSide(color: ColorManager.primary, width: AppSize.s1_5), 96 | borderRadius: BorderRadius.all(Radius.circular(AppSize.s8))), 97 | )); 98 | } 99 | -------------------------------------------------------------------------------- /lib/presentation/resources/values_manager.dart: -------------------------------------------------------------------------------- 1 | class AppMargin { 2 | static const double m8 = 8.0; 3 | static const double m12 = 12.0; 4 | static const double m14 = 14.0; 5 | static const double m16 = 16.0; 6 | static const double m18 = 18.0; 7 | static const double m20 = 20.0; 8 | } 9 | 10 | class AppPadding { 11 | static const double p2 = 2.0; 12 | static const double p8 = 8.0; 13 | static const double p12 = 12.0; 14 | static const double p14 = 14.0; 15 | static const double p16 = 16.0; 16 | static const double p18 = 18.0; 17 | static const double p20 = 20.0; 18 | static const double p24 = 24.0; 19 | static const double p28 = 28.0; 20 | static const double p30 = 30.0; 21 | static const double p60 = 60.0; 22 | static const double p100 = 100.0; 23 | } 24 | 25 | class AppSize { 26 | static const double s0 = 0; 27 | static const double s1 = 1; 28 | static const double s1_5 = 1.5; 29 | static const double s4 = 4.0; 30 | static const double s8 = 8.0; 31 | static const int si8 = 8; 32 | static const double s12 = 12.0; 33 | static const double s14 = 14.0; 34 | static const double s16 = 16.0; 35 | static const double s18 = 18.0; 36 | static const double s20 = 20.0; 37 | static const double s28 = 28.0; 38 | static const double s40 = 40.0; 39 | static const double s60 = 60.0; 40 | static const double s65 = 65.0; 41 | static const double s90 = 90.0; 42 | static const double s100 = 100.0; 43 | static const double s120 = 120.0; 44 | static const double s130 = 130.0; 45 | static const double s140 = 140.0; 46 | static const double s180 = 180.0; 47 | static const double s190 = 190.0; 48 | } 49 | 50 | class DurationConstant{ 51 | static const int d300 = 300; 52 | 53 | } -------------------------------------------------------------------------------- /lib/presentation/splash/splash.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:complete_advanced_flutter/app/app_prefs.dart'; 4 | import 'package:complete_advanced_flutter/app/di.dart'; 5 | import 'package:complete_advanced_flutter/presentation/resources/assets_manager.dart'; 6 | import 'package:complete_advanced_flutter/presentation/resources/color_manager.dart'; 7 | import 'package:complete_advanced_flutter/presentation/resources/routes_manager.dart'; 8 | import 'package:flutter/material.dart'; 9 | 10 | class SplashView extends StatefulWidget { 11 | const SplashView({Key? key}) : super(key: key); 12 | 13 | @override 14 | _SplashViewState createState() => _SplashViewState(); 15 | } 16 | 17 | class _SplashViewState extends State { 18 | Timer? _timer; 19 | AppPreferences _appPreferences = instance(); 20 | 21 | _startDelay() { 22 | _timer = Timer(Duration(seconds: 2), _goNext); 23 | } 24 | 25 | _goNext() async { 26 | _appPreferences.isUserLoggedIn().then((isUserLoggedIn) => { 27 | if (isUserLoggedIn) 28 | { 29 | // navigate to main screen 30 | Navigator.pushReplacementNamed(context, Routes.mainRoute) 31 | } 32 | else 33 | { 34 | _appPreferences 35 | .isOnBoardingScreenViewed() 36 | .then((isOnBoardingScreenViewed) => { 37 | if (isOnBoardingScreenViewed) 38 | { 39 | Navigator.pushReplacementNamed( 40 | context, Routes.loginRoute) 41 | } 42 | else 43 | { 44 | Navigator.pushReplacementNamed( 45 | context, Routes.onBoardingRoute) 46 | } 47 | }) 48 | } 49 | }); 50 | } 51 | 52 | @override 53 | void initState() { 54 | super.initState(); 55 | _startDelay(); 56 | } 57 | 58 | @override 59 | void dispose() { 60 | _timer?.cancel(); 61 | super.dispose(); 62 | } 63 | 64 | @override 65 | Widget build(BuildContext context) { 66 | return Scaffold( 67 | backgroundColor: ColorManager.primary, 68 | body: Center( 69 | child: Image( 70 | image: AssetImage(ImageAssets.splashLogo), 71 | ), 72 | ), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/presentation/store_details/store_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/app/di.dart'; 2 | import 'package:complete_advanced_flutter/domain/model/model.dart'; 3 | import 'package:complete_advanced_flutter/presentation/common/state_renderer/state_render_impl.dart'; 4 | import 'package:complete_advanced_flutter/presentation/resources/color_manager.dart'; 5 | import 'package:complete_advanced_flutter/presentation/resources/strings_manager.dart'; 6 | import 'package:complete_advanced_flutter/presentation/resources/values_manager.dart'; 7 | import 'package:complete_advanced_flutter/presentation/store_details/store_details_viewmodel.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:easy_localization/easy_localization.dart'; 10 | 11 | class StoreDetailsView extends StatefulWidget { 12 | const StoreDetailsView({Key? key}) : super(key: key); 13 | 14 | @override 15 | _StoreDetailsViewState createState() => _StoreDetailsViewState(); 16 | } 17 | 18 | class _StoreDetailsViewState extends State { 19 | final StoreDetailsViewModel _viewModel = instance(); 20 | 21 | @override 22 | void initState() { 23 | bind(); 24 | super.initState(); 25 | } 26 | 27 | bind() { 28 | _viewModel.start(); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return Scaffold( 34 | body: StreamBuilder( 35 | stream: _viewModel.outputState, 36 | builder: (context, snapshot) { 37 | return Scaffold( 38 | body: 39 | snapshot.data?.getScreenWidget(context, _getContentWidget(), () { 40 | _viewModel.start(); 41 | }) ?? 42 | Container(), 43 | ); 44 | }, 45 | )); 46 | } 47 | 48 | Widget _getContentWidget() { 49 | return Scaffold( 50 | backgroundColor: ColorManager.white, 51 | appBar: AppBar( 52 | title: Text(AppStrings.storeDetails.tr()), 53 | elevation: AppSize.s0, 54 | iconTheme: IconThemeData( 55 | //back button 56 | color: ColorManager.white, 57 | ), 58 | backgroundColor: ColorManager.primary, 59 | centerTitle: true, 60 | ), 61 | body: Container( 62 | constraints: BoxConstraints.expand(), 63 | color: ColorManager.white, 64 | child: SingleChildScrollView( 65 | child: StreamBuilder( 66 | stream: _viewModel.outputStoreDetails, 67 | builder: (context, snapshot) { 68 | return _getItems(snapshot.data); 69 | }, 70 | ), 71 | ), 72 | )); 73 | } 74 | 75 | Widget _getItems(StoreDetails? storeDetails) { 76 | if (storeDetails != null) { 77 | return Column( 78 | crossAxisAlignment: CrossAxisAlignment.start, 79 | children: [ 80 | Center( 81 | child: Image.network( 82 | storeDetails.image, 83 | fit: BoxFit.cover, 84 | width: double.infinity, 85 | height: 250, 86 | )), 87 | _getSection(AppStrings.details.tr()), 88 | _getInfoText(storeDetails.details), 89 | _getSection(AppStrings.services.tr()), 90 | _getInfoText(storeDetails.services), 91 | _getSection(AppStrings.about.tr()), 92 | _getInfoText(storeDetails.about) 93 | ], 94 | ); 95 | } else { 96 | return Container(); 97 | } 98 | } 99 | 100 | Widget _getSection(String title) { 101 | return Padding( 102 | padding: const EdgeInsets.only( 103 | top: AppPadding.p12, 104 | left: AppPadding.p12, 105 | right: AppPadding.p12, 106 | bottom: AppPadding.p2), 107 | child: Text(title, style: Theme.of(context).textTheme.headline3)); 108 | } 109 | 110 | Widget _getInfoText(String info) { 111 | return Padding( 112 | padding: const EdgeInsets.all(AppSize.s12), 113 | child: Text(info, style: Theme.of(context).textTheme.bodyText2), 114 | ); 115 | } 116 | 117 | @override 118 | void dispose() { 119 | _viewModel.dispose(); 120 | super.dispose(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/presentation/store_details/store_details_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ffi'; 2 | 3 | import 'package:complete_advanced_flutter/domain/model/model.dart'; 4 | import 'package:complete_advanced_flutter/domain/usecase/store_details_usecase.dart'; 5 | import 'package:complete_advanced_flutter/presentation/base/baseviewmodel.dart'; 6 | import 'package:complete_advanced_flutter/presentation/common/state_renderer/state_render_impl.dart'; 7 | import 'package:complete_advanced_flutter/presentation/common/state_renderer/state_renderer.dart'; 8 | import 'package:rxdart/rxdart.dart'; 9 | 10 | class StoreDetailsViewModel extends BaseViewModel 11 | with StoreDetailsViewModelInput, StoreDetailsViewModelOutput { 12 | final _storeDetailsStreamController = BehaviorSubject(); 13 | 14 | final StoreDetailsUseCase storeDetailsUseCase; 15 | 16 | StoreDetailsViewModel(this.storeDetailsUseCase); 17 | 18 | @override 19 | start() async { 20 | _loadData(); 21 | } 22 | 23 | _loadData() async { 24 | inputState.add(LoadingState( 25 | stateRendererType: StateRendererType.FULL_SCREEN_LOADING_STATE)); 26 | (await storeDetailsUseCase.execute(Void)).fold( 27 | (failure) { 28 | inputState.add(ErrorState( 29 | StateRendererType.FULL_SCREEN_ERROR_STATE, failure.message)); 30 | }, 31 | (storeDetails) async { 32 | inputState.add(ContentState()); 33 | inputStoreDetails.add(storeDetails); 34 | }, 35 | ); 36 | } 37 | 38 | @override 39 | void dispose() { 40 | _storeDetailsStreamController.close(); 41 | } 42 | 43 | @override 44 | Sink get inputStoreDetails => _storeDetailsStreamController.sink; 45 | 46 | //output 47 | @override 48 | Stream get outputStoreDetails => 49 | _storeDetailsStreamController.stream.map((stores) => stores); 50 | } 51 | 52 | abstract class StoreDetailsViewModelInput { 53 | Sink get inputStoreDetails; 54 | } 55 | 56 | abstract class StoreDetailsViewModelOutput { 57 | Stream get outputStoreDetails; 58 | } 59 | -------------------------------------------------------------------------------- /lib/test.dart: -------------------------------------------------------------------------------- 1 | import 'package:complete_advanced_flutter/app/app.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class Test extends StatelessWidget { 5 | const Test({Key? key}) : super(key: key); 6 | 7 | void updateAppState(){ 8 | MyApp.instance.appState =10; 9 | } 10 | void getAppState(){ 11 | print( MyApp.instance.appState); // 10 12 | } 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Container(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: complete_advanced_flutter 2 | description: A new Flutter application. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.12.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | flutter_svg: ^0.22.0 27 | retrofit: ^2.0.1 28 | analyzer: ^1.7.0 29 | dio: ^4.0.0 30 | json_serializable: ^4.1.4 31 | # The following adds the Cupertino Icons font to your application. 32 | # Use with the CupertinoIcons class for iOS style icons. 33 | cupertino_icons: ^1.0.2 34 | dartz: ^0.10.0 35 | data_connection_checker: ^0.3.4 36 | pretty_dio_logger: ^1.2.0-beta-1 37 | shared_preferences: ^2.0.8 38 | device_info: ^2.0.2 39 | freezed: ^0.14.2 40 | get_it: ^7.2.0 41 | lottie: ^1.2.1 42 | country_code_picker: ^2.0.2 43 | image_picker: ^0.8.4+4 44 | rxdart: ^0.27.2 45 | carousel_slider: ^4.0.0 46 | easy_localization: ^3.0.0 47 | flutter_phoenix: ^1.0.0 48 | 49 | dev_dependencies: 50 | flutter_test: 51 | sdk: flutter 52 | retrofit_generator: ^2.0.1 53 | build_runner: ^2.1.4 54 | 55 | # For information on the generic Dart part of this file, see the 56 | # following page: https://dart.dev/tools/pub/pubspec 57 | 58 | # The following section is specific to Flutter. 59 | flutter: 60 | 61 | # The following line ensures that the Material Icons font is 62 | # included with your application, so that you can use the icons in 63 | # the material Icons class. 64 | uses-material-design: true 65 | 66 | # To add assets to your application, add an assets section, like this: 67 | assets: 68 | - assets/images/splash_logo.png 69 | - assets/images/onboarding_logo1.svg 70 | - assets/images/onboarding_logo2.svg 71 | - assets/images/onboarding_logo3.svg 72 | - assets/images/onboarding_logo4.svg 73 | - assets/images/hollow_cirlce_ic.svg 74 | - assets/images/left_arrow_ic.svg 75 | - assets/images/right_arrow_ic.svg 76 | - assets/images/solid_circle_ic.svg 77 | - assets/images/photo_camera_ic.svg 78 | - assets/images/settings_right_arrow_ic.svg 79 | - assets/images/change_lang_ic.svg 80 | - assets/images/contact_us_ic.svg 81 | - assets/images/invite_friends_ic.svg 82 | - assets/images/logout_ic.svg 83 | - assets/json/loading.json 84 | - assets/json/error.json 85 | - assets/json/empty.json 86 | - assets/json/success.json 87 | - assets/translations/ 88 | 89 | # An image asset can refer to one or more resolution-specific "variants", see 90 | # https://flutter.dev/assets-and-images/#resolution-aware. 91 | 92 | # For details regarding adding assets from package dependencies, see 93 | # https://flutter.dev/assets-and-images/#from-packages 94 | 95 | # To add custom fonts to your application, add a fonts section here, 96 | # in this "flutter" section. Each entry in this list should have a 97 | # "family" key with the font family name, and a "fonts" key with a 98 | # list giving the asset and other descriptors for the font. For 99 | # example: 100 | fonts: 101 | - family: Montserrat 102 | fonts: 103 | - asset: assets/fonts/Montserrat-Bold.ttf 104 | weight: 700 105 | - asset: assets/fonts/Montserrat-SemiBold.ttf 106 | weight: 600 107 | - asset: assets/fonts/Montserrat-Medium.ttf 108 | weight: 500 109 | - asset: assets/fonts/Montserrat-Regular.ttf 110 | weight: 400 111 | - asset: assets/fonts/Montserrat-Light.ttf 112 | weight: 300 113 | # 114 | # For details regarding fonts from package dependencies, 115 | # see https://flutter.dev/custom-fonts/#from-packages 116 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:complete_advanced_flutter/app/app.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | 12 | import 'package:complete_advanced_flutter/main.dart'; 13 | 14 | void main() { 15 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 16 | // Build our app and trigger a frame. 17 | await tester.pumpWidget(MyApp()); 18 | 19 | // Verify that our counter starts at 0. 20 | expect(find.text('0'), findsOneWidget); 21 | expect(find.text('1'), findsNothing); 22 | 23 | // Tap the '+' icon and trigger a frame. 24 | await tester.tap(find.byIcon(Icons.add)); 25 | await tester.pump(); 26 | 27 | // Verify that our counter has incremented. 28 | expect(find.text('0'), findsNothing); 29 | expect(find.text('1'), findsOneWidget); 30 | }); 31 | } 32 | --------------------------------------------------------------------------------