diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore index 0a741cb..6f56801 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -9,3 +9,5 @@ GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle index 60a2fd4..530eca0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,21 +26,26 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion 32 - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' } - lintOptions { - disable 'InvalidPackage' + sourceSets { + main.java.srcDirs += 'src/main/kotlin' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.zarvilla.all_flutter_gives" - minSdkVersion 16 - targetSdkVersion 28 + minSdkVersion 20 + targetSdkVersion 32 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c86c802..2495e0b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,14 +1,14 @@ - - + + + + + diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..449a9f9 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 1f83a33..d74aa35 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -1,7 +1,7 @@ - - diff --git a/android/build.gradle b/android/build.gradle index 3100ad2..15bb387 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.5.0' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -14,15 +14,13 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { project.evaluationDependsOn(':app') } diff --git a/android/gradle.properties b/android/gradle.properties index 38c8d45..94adc3a 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M -android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index c0c3574..bc6a58a 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://FlutterWeb.services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/assets/images/avatar.png b/assets/images/avatar.png new file mode 100644 index 0000000..e306e4b Binary files /dev/null and b/assets/images/avatar.png differ diff --git a/assets/images/burger.png b/assets/images/burger.png new file mode 100644 index 0000000..1a7bb13 Binary files /dev/null and b/assets/images/burger.png differ diff --git a/assets/images/desert.png b/assets/images/desert.png new file mode 100644 index 0000000..b7e13c0 Binary files /dev/null and b/assets/images/desert.png differ diff --git a/assets/images/drinks.png b/assets/images/drinks.png new file mode 100644 index 0000000..a60c3fd Binary files /dev/null and b/assets/images/drinks.png differ diff --git a/assets/images/fastfood.png b/assets/images/fastfood.png new file mode 100644 index 0000000..c6e4479 Binary files /dev/null and b/assets/images/fastfood.png differ diff --git a/assets/images/fries.png b/assets/images/fries.png new file mode 100644 index 0000000..4c93337 Binary files /dev/null and b/assets/images/fries.png differ diff --git a/assets/images/hotdog.png b/assets/images/hotdog.png new file mode 100644 index 0000000..f39d683 Binary files /dev/null and b/assets/images/hotdog.png differ diff --git a/assets/images/pizza.png b/assets/images/pizza.png new file mode 100644 index 0000000..7cefc53 Binary files /dev/null and b/assets/images/pizza.png differ diff --git a/assets/images/salad.png b/assets/images/salad.png new file mode 100644 index 0000000..250c163 Binary files /dev/null and b/assets/images/salad.png differ diff --git a/assets/images/tree_v.png b/assets/images/tree_v.png new file mode 100644 index 0000000..09ad405 Binary files /dev/null and b/assets/images/tree_v.png differ diff --git a/assets/resources.zip b/assets/resources.zip deleted file mode 100644 index 1a4ae1b..0000000 Binary files a/assets/resources.zip and /dev/null differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ba085ac..b4e2e62 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,22 +1,57 @@ PODS: + - connectivity (0.0.1): + - Flutter + - Reachability - Flutter (1.0.0) + - fluttertoast (0.0.2): + - Flutter + - Toast + - integration_test (0.0.1): + - Flutter - path_provider (0.0.1): - Flutter + - Reachability (3.2) + - shared_preferences (0.0.1): + - Flutter + - Toast (4.0.0) DEPENDENCIES: + - connectivity (from `.symlinks/plugins/connectivity/ios`) - Flutter (from `Flutter`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) + - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) + +SPEC REPOS: + trunk: + - Reachability + - Toast EXTERNAL SOURCES: + connectivity: + :path: ".symlinks/plugins/connectivity/ios" Flutter: :path: Flutter + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" + shared_preferences: + :path: ".symlinks/plugins/shared_preferences/ios" SPEC CHECKSUMS: - Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467 + Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58 + integration_test: 5ed24a436eb7ec17b6a13046e9bf7ca4a404e59e path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c + Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 + shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d + Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c -COCOAPODS: 1.10.0 +COCOAPODS: 1.11.2 diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a1..919434a 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 70693e4..47b9837 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ import UIKit import Flutter +import GoogleMaps @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { @@ -7,7 +8,8 @@ import Flutter _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + GMSServices.provideAPIKey("AIzaSyC9UyXEkIayXCAUkMOxA0nDKqyfCTyLKUo") GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } -} +} \ No newline at end of file diff --git a/lib/CachingApiData/api_call_http.dart b/lib/CachingApiData/api_call_http.dart index 138cb64..7c02baf 100644 --- a/lib/CachingApiData/api_call_http.dart +++ b/lib/CachingApiData/api_call_http.dart @@ -29,7 +29,7 @@ class ApiCall { else { print("Loading from API"); - var response = await http.get(API_URL); + var response = await http.get(Uri.parse(API_URL)); if (response.statusCode == 200) { var jsonResponse = response.body; diff --git a/lib/CleanArchitectureTDD/core/error/failures.dart b/lib/CleanArchitectureTDD/core/error/failures.dart new file mode 100644 index 0000000..306354e --- /dev/null +++ b/lib/CleanArchitectureTDD/core/error/failures.dart @@ -0,0 +1,7 @@ +import 'package:equatable/equatable.dart'; + +abstract class Failure extends Equatable { + // If the subclasses have some properties, they'll get passed to this constructor + // so that Equatable can perform value comparison. + Failure([List properties = const []]) : super(); +} \ No newline at end of file diff --git a/lib/CleanArchitectureTDD/features/number_trivia/domain/entities/number_trivia.dart b/lib/CleanArchitectureTDD/features/number_trivia/domain/entities/number_trivia.dart new file mode 100644 index 0000000..a28e5b4 --- /dev/null +++ b/lib/CleanArchitectureTDD/features/number_trivia/domain/entities/number_trivia.dart @@ -0,0 +1,13 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +class NumberTrivia extends Equatable { + final String text; + final int number; + + NumberTrivia({@required this.text, @required this.number}) : super(); + + @override + // TODO: implement props + List get props => throw UnimplementedError(); +} diff --git a/lib/CleanArchitectureTDD/features/number_trivia/domain/repositories/number_trivia_repository.dart b/lib/CleanArchitectureTDD/features/number_trivia/domain/repositories/number_trivia_repository.dart new file mode 100644 index 0000000..01a1a59 --- /dev/null +++ b/lib/CleanArchitectureTDD/features/number_trivia/domain/repositories/number_trivia_repository.dart @@ -0,0 +1,9 @@ + +import 'package:all_flutter_gives/CleanArchitectureTDD/core/error/failures.dart'; +import 'package:all_flutter_gives/CleanArchitectureTDD/features/number_trivia/domain/entities/number_trivia.dart'; +import 'package:dartz/dartz.dart'; + +abstract class NumberTriviaRepository { + Future> getConcreteNumberTrivia(int number); + Future> getRandomNumberTrivia(); +} \ No newline at end of file diff --git a/lib/CleanArchitectureTDD/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart b/lib/CleanArchitectureTDD/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart new file mode 100644 index 0000000..5b552b2 --- /dev/null +++ b/lib/CleanArchitectureTDD/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart @@ -0,0 +1,15 @@ +import 'package:all_flutter_gives/CleanArchitectureTDD/core/error/failures.dart'; +import 'package:all_flutter_gives/CleanArchitectureTDD/features/number_trivia/domain/entities/number_trivia.dart'; +import 'package:all_flutter_gives/CleanArchitectureTDD/features/number_trivia/domain/repositories/number_trivia_repository.dart'; +import 'package:dartz/dartz.dart'; +import 'package:meta/meta.dart'; + +class GetConcreteNumberTrivia { + final NumberTriviaRepository repository; + + GetConcreteNumberTrivia(this.repository); + + Future> call({@required int number}) async{ + return await repository.getConcreteNumberTrivia(number); + } +} diff --git a/lib/FlutterIntTesting/integration_test/app_test.dart b/lib/FlutterIntTesting/integration_test/app_test.dart new file mode 100644 index 0000000..a73169c --- /dev/null +++ b/lib/FlutterIntTesting/integration_test/app_test.dart @@ -0,0 +1,48 @@ +import 'package:all_flutter_gives/FlutterIntTesting/main.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets( + "Not inputting a text and wanting to go to the display page shows " + "an error and prevents from going to the display page.", + (WidgetTester tester) async { + await tester.pumpWidget(MyApp()); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + + expect(find.byType(TypingPage), findsOneWidget); + expect(find.byType(DisplayPage), findsNothing); + expect(find.text('Input at least one character'), findsOneWidget); + }, + ); + + testWidgets( + "After inputting a text, go to the display page which contains that same text " + "and then navigate back to the typing page where the input should be clear", + (WidgetTester tester) async { + await tester.pumpWidget(MyApp()); + + final inputText = 'Hello there, this is an input.'; + await tester.enterText(find.byKey(Key('your-text-field')), inputText); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + + expect(find.byType(TypingPage), findsNothing); + expect(find.byType(DisplayPage), findsOneWidget); + expect(find.text(inputText), findsOneWidget); + + await tester.tap(find.byType(BackButton)); + await tester.pumpAndSettle(); + + expect(find.byType(TypingPage), findsOneWidget); + expect(find.byType(DisplayPage), findsNothing); + expect(find.text(inputText), findsNothing); + }, + ); +} diff --git a/lib/FlutterIntTesting/main.dart b/lib/FlutterIntTesting/main.dart new file mode 100644 index 0000000..842643c --- /dev/null +++ b/lib/FlutterIntTesting/main.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; + +void main() => runApp(MyApp()); + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Typing App', + home: TypingPage(), + ); + } +} + +class TypingPage extends StatefulWidget { + TypingPage({Key key}) : super(key: key); + + @override + _TypingPageState createState() => _TypingPageState(); +} + +class _TypingPageState extends State { + TextEditingController _controller; + final GlobalKey _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Typing'), + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Form( + key: _formKey, + child: TextFormField( + key: Key('your-text-field'), + controller: _controller, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: 'Your Text', + ), + validator: (value) => + value.isEmpty ? 'Input at least one character' : null, + ), + ), + ), + ), + floatingActionButton: FloatingActionButton( + child: Icon(Icons.arrow_forward), + onPressed: () { + if (_formKey.currentState.validate()) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) { + return DisplayPage( + displayText: _controller.text, + doOnInit: () => Future.microtask(() => _controller.clear()), + ); + }, + ), + ); + } + }, + ), + ); + } +} + +class DisplayPage extends StatefulWidget { + final String displayText; + final void Function() doOnInit; + + const DisplayPage({ + @required this.displayText, + @required this.doOnInit, + Key key, + }) : super(key: key); + + @override + _DisplayPageState createState() => _DisplayPageState(); +} + +class _DisplayPageState extends State { + @override + void initState() { + super.initState(); + widget.doOnInit(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Display'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Center( + child: Text( + widget.displayText, + style: TextStyle(fontSize: 32), + ), + ), + ), + ); + } +} diff --git a/lib/FlutterIntTesting/test_driver/integration_driver.dart b/lib/FlutterIntTesting/test_driver/integration_driver.dart new file mode 100644 index 0000000..b38629c --- /dev/null +++ b/lib/FlutterIntTesting/test_driver/integration_driver.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/lib/FlutterMockTesting/UI/app_theme.dart b/lib/FlutterMockTesting/UI/app_theme.dart new file mode 100755 index 0000000..f239c96 --- /dev/null +++ b/lib/FlutterMockTesting/UI/app_theme.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + static ThemeData get data { + return ThemeData( + textTheme: TextTheme( + subtitle1: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20.0, + ), + subtitle2: TextStyle( + fontWeight: FontWeight.w300, + fontSize: 18.0, + ), + ), + ); + } +} diff --git a/lib/FlutterMockTesting/UI/views/base_view.dart b/lib/FlutterMockTesting/UI/views/base_view.dart new file mode 100755 index 0000000..bd15609 --- /dev/null +++ b/lib/FlutterMockTesting/UI/views/base_view.dart @@ -0,0 +1,34 @@ +import 'package:all_flutter_gives/FlutterMockTesting/core/viewmodels/base_model.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/helpers/dependency_assembly.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class BaseView extends StatefulWidget { + final Widget Function(BuildContext context, T model, Widget child) builder; + final Function(T) onModelReady; + + BaseView({this.builder, this.onModelReady}); + + @override + _BaseViewState createState() => _BaseViewState(); +} + +class _BaseViewState extends State> { + T model = dependencyAssembler(); + + @override + void initState() { + if (widget.onModelReady != null) { + widget.onModelReady(model); + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => model, + child: Consumer(builder: widget.builder), + ); + } +} diff --git a/lib/FlutterMockTesting/UI/views/cart_view.dart b/lib/FlutterMockTesting/UI/views/cart_view.dart new file mode 100755 index 0000000..361855a --- /dev/null +++ b/lib/FlutterMockTesting/UI/views/cart_view.dart @@ -0,0 +1,108 @@ +import 'package:all_flutter_gives/FlutterMockTesting/UI/widgets/cart_item_card.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/viewmodels/cart_model.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/helpers/constants.dart'; +import 'package:flutter/material.dart'; + +class CartView extends StatelessWidget { + final CartModel model; + + CartView({this.model}); + + _showConfirmationAlertDialog(BuildContext context) { + Widget confirmButton = FlatButton( + child: Text("Confirm"), + onPressed: () { + Navigator.of(context).pop(true); + }, + ); + Widget cancelButton = FlatButton( + child: Text("Cancel"), + onPressed: () { + Navigator.of(context).pop(false); + }, + ); + AlertDialog alert = AlertDialog( + title: Text("Confirm Purchase?"), + content: Text("Grant Total: \$${model.totalCost}"), + actions: [ + cancelButton, + confirmButton, + ], + ); + showDialog( + context: context, + builder: (BuildContext context) { + return alert; + }, + ).then((confirmedPurchased) { + if (confirmedPurchased) { + _showConfirmedAlertDialog(context); + } + }); + } + + _showConfirmedAlertDialog(BuildContext context) { + Widget okButton = FlatButton( + child: Text("Ok"), + onPressed: () { + model.clearCart(); + Navigator.of(context).pop(); + }, + ); + AlertDialog alert = AlertDialog( + title: Text("Ordered Confirmed"), + actions: [ + okButton, + ], + ); + showDialog( + context: context, + builder: (BuildContext context) { + return alert; + }, + ).then((_) { + Navigator.of(context).pop(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey.shade50, + appBar: AppBar( + title: Text(ViewTitle.Cart), + actions: [ + model.cartSize > 0 + ? IconButton( + icon: Icon(Icons.done), + onPressed: () async { + _showConfirmationAlertDialog(context); + }, + ) + : Container() + ], + ), + body: model.cartSize > 0 + ? ListView.builder( + itemBuilder: (BuildContext context, int index) { + return Column( + children: [ + Padding( + child: CartItemCard(model.getProduct(index), + model.getProductQuantity(index)), + padding: EdgeInsets.all(10.0), + ), + ], + ); + }, + itemCount: model.getCartSummary().keys.length, + ) + : Center( + child: Text( + 'Your cart is empty', + style: Theme.of(context).textTheme.subtitle1, + ), + ), + ); + } +} diff --git a/lib/FlutterMockTesting/UI/views/product_list_view.dart b/lib/FlutterMockTesting/UI/views/product_list_view.dart new file mode 100755 index 0000000..8c0423f --- /dev/null +++ b/lib/FlutterMockTesting/UI/views/product_list_view.dart @@ -0,0 +1,51 @@ +import 'package:all_flutter_gives/FlutterMockTesting/UI/widgets/cart_count_badge.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/UI/widgets/product_list.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/enums/view_state.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/viewmodels/cart_model.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/viewmodels/product_list_model.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/helpers/constants.dart'; +import 'package:flutter/material.dart'; + +import 'base_view.dart'; +import 'cart_view.dart'; + +class ProductListView extends StatelessWidget { + Widget _buildCartButton(BuildContext context, CartModel cartModel) { + return Stack( + children: [ + IconButton( + icon: Icon(Icons.shopping_cart), + splashColor: Colors.blue, + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => CartView(model: cartModel)), + ); + }), + cartModel.cartSize != 0 + ? CartCountBadge(cartModel.cartSize) + : Container() + ], + ); + } + + @override + Widget build(BuildContext context) { + return BaseView( + onModelReady: (cartModel) => cartModel.getCart(), + builder: (context, cartModel, child) => BaseView( + onModelReady: (model) => model.getProducts(), + builder: (context, model, child) => Scaffold( + backgroundColor: Colors.grey.shade50, + appBar: AppBar( + title: Text(ViewTitle.ProductList), + actions: [_buildCartButton(context, cartModel)], + ), + body: model.state == ViewState.Busy + ? Center(child: CircularProgressIndicator()) + : ProductList(model.products, cartModel), + ), + ), + ); + } +} diff --git a/lib/FlutterMockTesting/UI/widgets/cart_count_badge.dart b/lib/FlutterMockTesting/UI/widgets/cart_count_badge.dart new file mode 100755 index 0000000..dfa69d4 --- /dev/null +++ b/lib/FlutterMockTesting/UI/widgets/cart_count_badge.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class CartCountBadge extends StatelessWidget { + const CartCountBadge(this.cartSize); + + final int cartSize; + + @override + Widget build(BuildContext context) { + return Positioned( + right: 11, + top: 7, + child: Container( + padding: EdgeInsets.all(2), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(6), + ), + constraints: BoxConstraints( + minWidth: 14, + minHeight: 14, + ), + child: Text( + '$cartSize', + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ), + ); + } +} diff --git a/lib/FlutterMockTesting/UI/widgets/cart_item_card.dart b/lib/FlutterMockTesting/UI/widgets/cart_item_card.dart new file mode 100755 index 0000000..db6e86b --- /dev/null +++ b/lib/FlutterMockTesting/UI/widgets/cart_item_card.dart @@ -0,0 +1,53 @@ +import 'package:all_flutter_gives/FlutterMockTesting/UI/widgets/product_image.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/models/product.dart'; +import 'package:flutter/material.dart'; + +class CartItemCard extends StatelessWidget { + const CartItemCard(this.product, this.quantity); + + final Product product; + final int quantity; + + List _getProductDetails(BuildContext context) { + return [ + Text(product.name, + maxLines: 2, style: Theme.of(context).textTheme.subtitle1), + Text('\$${product.price.toString()}.00', + style: Theme.of(context).textTheme.headline1), + Text('\Qty: ${quantity.toString()}', + style: Theme.of(context).textTheme.subtitle1) + ]; + } + + @override + Widget build(BuildContext context) { + return Card( + elevation: 4.0, + margin: new EdgeInsets.symmetric(horizontal: 20.0, vertical: 6.0), + child: Container( + height: 150.0, + decoration: BoxDecoration(color: Colors.white), + child: ListTile( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 1, + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: _getProductDetails(context), + ), + ), + ), + VerticalDivider(), + ProductImage(product.imageUrl), + ], + ), + ), + ), + ); + } +} diff --git a/lib/FlutterMockTesting/UI/widgets/product_card.dart b/lib/FlutterMockTesting/UI/widgets/product_card.dart new file mode 100755 index 0000000..0d9c172 --- /dev/null +++ b/lib/FlutterMockTesting/UI/widgets/product_card.dart @@ -0,0 +1,57 @@ +import 'package:all_flutter_gives/FlutterMockTesting/UI/widgets/product_image.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/models/product.dart'; +import 'package:flutter/material.dart'; + +class ProductCard extends StatelessWidget { + const ProductCard(this.product); + + final Product product; + + List _getProductDetails(BuildContext context) { + return [ + Text(product.name, maxLines: 2, style: Theme.of(context).textTheme.headline1), + Row( + children: [ + Text('\$${product.price.toString()}.00', + style: Theme.of(context).textTheme.subtitle1), + IconButton( + icon: Icon(Icons.add_shopping_cart), + color: Colors.grey, + onPressed: () {}, + ) + ], + ), + ]; + } + + @override + Widget build(BuildContext context) { + return Card( + elevation: 4.0, + margin: EdgeInsets.symmetric(horizontal: 20.0, vertical: 6.0), + child: Container( + height: 150.0, + decoration: BoxDecoration(color: Colors.white), + child: ListTile( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 1, + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: _getProductDetails(context)), + ), + ), + VerticalDivider(), + ProductImage(product.imageUrl) + ], + ), + ), + ), + ); + } +} diff --git a/lib/FlutterMockTesting/UI/widgets/product_image.dart b/lib/FlutterMockTesting/UI/widgets/product_image.dart new file mode 100755 index 0000000..ae04fd7 --- /dev/null +++ b/lib/FlutterMockTesting/UI/widgets/product_image.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class ProductImage extends StatelessWidget { + final String imageUrl; + + const ProductImage(this.imageUrl); + + @override + Widget build(BuildContext context) { + return Image.network( + imageUrl, + height: 125.0, + width: 135.0, + fit: BoxFit.fitHeight, + ); + } +} diff --git a/lib/FlutterMockTesting/UI/widgets/product_list.dart b/lib/FlutterMockTesting/UI/widgets/product_list.dart new file mode 100755 index 0000000..f56869c --- /dev/null +++ b/lib/FlutterMockTesting/UI/widgets/product_list.dart @@ -0,0 +1,32 @@ +import 'package:all_flutter_gives/FlutterMockTesting/UI/widgets/product_card.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/models/product.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/viewmodels/cart_model.dart'; +import 'package:flutter/material.dart'; + +class ProductList extends StatelessWidget { + const ProductList(this.products, this.cartModel); + + final List products; + final CartModel cartModel; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemBuilder: (BuildContext context, int index) { + return Column( + children: [ + Padding( + child: InkWell( + child: ProductCard(products[index]), + onTap: () { + cartModel.addToCart(products[index]); + }), + padding: EdgeInsets.all(10.0), + ), + ], + ); + }, + itemCount: products.length, + ); + } +} diff --git a/lib/FlutterMockTesting/UI/widgets/vertical_divider.dart b/lib/FlutterMockTesting/UI/widgets/vertical_divider.dart new file mode 100755 index 0000000..6da42d5 --- /dev/null +++ b/lib/FlutterMockTesting/UI/widgets/vertical_divider.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class VerticalDivider extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + color: Colors.grey.shade100, + width: 2.0, + height: 130.0, + ); + } +} diff --git a/lib/FlutterMockTesting/core/enums/view_state.dart b/lib/FlutterMockTesting/core/enums/view_state.dart new file mode 100755 index 0000000..08c8932 --- /dev/null +++ b/lib/FlutterMockTesting/core/enums/view_state.dart @@ -0,0 +1 @@ +enum ViewState { Idle, Busy } \ No newline at end of file diff --git a/lib/FlutterMockTesting/core/models/product.dart b/lib/FlutterMockTesting/core/models/product.dart new file mode 100755 index 0000000..52f9852 --- /dev/null +++ b/lib/FlutterMockTesting/core/models/product.dart @@ -0,0 +1,25 @@ +class Product { + int id; + String name; + int price; + String imageUrl; + + Product({this.id, this.name, this.price, this.imageUrl}); + + Product.fromJson(Map json) { + id = json['id']; + name = json['name']; + price = json['price']; + imageUrl = json['imageUrl']; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Product && + runtimeType == other.runtimeType && + name == other.name; + + @override + int get hashCode => name.hashCode; +} diff --git a/lib/FlutterMockTesting/core/services/api.dart b/lib/FlutterMockTesting/core/services/api.dart new file mode 100755 index 0000000..2689c71 --- /dev/null +++ b/lib/FlutterMockTesting/core/services/api.dart @@ -0,0 +1,24 @@ +import 'dart:convert'; + +import 'package:all_flutter_gives/FlutterMockTesting/core/models/product.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/helpers/constants.dart'; +import 'package:http/http.dart' as http; + +class API { + static const endpoint = URL.ProductList; + + var client = new http.Client(); + + Future> getProducts() async { + var products = List(); + var response = await client.get(Uri.parse('$endpoint' + 'products.json')); + + var data = json.decode(response.body) as List; + + for (var product in data) { + products.add(Product.fromJson(product)); + } + + return products; + } +} diff --git a/lib/FlutterMockTesting/core/viewmodels/base_model.dart b/lib/FlutterMockTesting/core/viewmodels/base_model.dart new file mode 100755 index 0000000..275e14c --- /dev/null +++ b/lib/FlutterMockTesting/core/viewmodels/base_model.dart @@ -0,0 +1,13 @@ +import 'package:all_flutter_gives/FlutterMockTesting/core/enums/view_state.dart'; +import 'package:flutter/material.dart'; + +class BaseModel extends ChangeNotifier { + ViewState _state = ViewState.Idle; + + ViewState get state => _state; + + void applyState(ViewState viewState) { + _state = viewState; + notifyListeners(); + } +} diff --git a/lib/FlutterMockTesting/core/viewmodels/cart_model.dart b/lib/FlutterMockTesting/core/viewmodels/cart_model.dart new file mode 100755 index 0000000..3b50fe7 --- /dev/null +++ b/lib/FlutterMockTesting/core/viewmodels/cart_model.dart @@ -0,0 +1,60 @@ + +import 'package:all_flutter_gives/FlutterMockTesting/core/models/product.dart'; + +import 'base_model.dart'; + +class CartModel extends BaseModel { + List cart = []; + Map> cartSummary = {}; + + int get cartSize { + return cart != null ? cart.length : 0; + } + + void addToCart(Product product) { + cart.add(product); + notifyListeners(); + } + + List getCart() { + return cart; + } + + Map> getCartSummary() { + cartSummary = {}; + cart.forEach((product) { + if (!cartSummary.keys.contains(product.name)) { + cartSummary[product.name] = [product]; + } else { + var currentProducts = cartSummary[product.name]; + currentProducts.add(product); + cartSummary[product.name] = currentProducts; + } + }); + return cartSummary; + } + + Product getProduct(int index) { + var name = cartSummary.keys.elementAt(index); + return cartSummary[name].first; + } + + int getProductQuantity(int index) { + var name = cartSummary.keys.elementAt(index); + return cartSummary[name].length; + } + + int get totalCost { + var cost = 0; + cartSummary.keys.forEach((productName) { + cost += (cartSummary[productName].first.price * + cartSummary[productName].length); + }); + return cost; + } + + void clearCart() { + cart = []; + notifyListeners(); + } +} diff --git a/lib/FlutterMockTesting/core/viewmodels/product_list_model.dart b/lib/FlutterMockTesting/core/viewmodels/product_list_model.dart new file mode 100755 index 0000000..fd5ef84 --- /dev/null +++ b/lib/FlutterMockTesting/core/viewmodels/product_list_model.dart @@ -0,0 +1,23 @@ + +import 'package:all_flutter_gives/FlutterMockTesting/core/enums/view_state.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/models/product.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/services/api.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/helpers/dependency_assembly.dart'; + +import 'base_model.dart'; + +class ProductListModel extends BaseModel { + API api = dependencyAssembler(); + + List _products; + + List get products { + return _products; + } + + Future getProducts() async { + applyState(ViewState.Busy); + _products = await api.getProducts(); + applyState(ViewState.Idle); + } +} diff --git a/lib/FlutterMockTesting/helpers/constants.dart b/lib/FlutterMockTesting/helpers/constants.dart new file mode 100755 index 0000000..8933e1c --- /dev/null +++ b/lib/FlutterMockTesting/helpers/constants.dart @@ -0,0 +1,9 @@ +class ViewTitle { + static const ProductList = "ShopNBuy"; + static const Cart = "ShopNBuy's Cart"; +} + +class URL { + // TODO: Place your own Firebase URL here + static const ProductList = "https://shopnbuy-25ef7.firebaseio.com/"; +} diff --git a/lib/FlutterMockTesting/helpers/dependency_assembly.dart b/lib/FlutterMockTesting/helpers/dependency_assembly.dart new file mode 100755 index 0000000..f321454 --- /dev/null +++ b/lib/FlutterMockTesting/helpers/dependency_assembly.dart @@ -0,0 +1,12 @@ +import 'package:all_flutter_gives/FlutterMockTesting/core/services/api.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/viewmodels/cart_model.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/viewmodels/product_list_model.dart'; +import 'package:get_it/get_it.dart'; + +GetIt dependencyAssembler = GetIt.instance; + +void setupDependencyAssembler() { + dependencyAssembler.registerLazySingleton(() => API()); + dependencyAssembler.registerFactory(() => ProductListModel()); + dependencyAssembler.registerFactory(() => CartModel()); +} diff --git a/lib/FlutterMockTesting/main.dart b/lib/FlutterMockTesting/main.dart new file mode 100755 index 0000000..c18eab2 --- /dev/null +++ b/lib/FlutterMockTesting/main.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +import 'UI/app_theme.dart'; +import 'UI/views/product_list_view.dart'; +import 'helpers/dependency_assembly.dart'; + +void main() { + setupDependencyAssembler(); + + runApp(ShopNBuyApp()); +} + +class ShopNBuyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: AppTheme.data, + home: ProductListView(), + ); + } +} diff --git a/lib/FlutterUnitTesting/auth.dart b/lib/FlutterUnitTesting/auth.dart new file mode 100644 index 0000000..ab586ed --- /dev/null +++ b/lib/FlutterUnitTesting/auth.dart @@ -0,0 +1,37 @@ + + +// import 'package:firebase_auth/firebase_auth.dart'; + +abstract class BaseAuth { + Future signInWithEmailAndPassword(String email, String password); + Future createUserWithEmailAndPassword(String email, String password); + Future currentUser(); + Future signOut(); +} + +class Auth implements BaseAuth { + // final FirebaseAuth _firebaseAuth = FirebaseAuth.instance; + + @override + Future signInWithEmailAndPassword(String email, String password) async { + // final FirebaseUser user = await _firebaseAuth.signInWithEmailAndPassword(email: email, password: password); + // return user?.uid; + } + + @override + Future createUserWithEmailAndPassword(String email, String password) async { + // final FirebaseUser user = await _firebaseAuth.createUserWithEmailAndPassword(email: email, password: password); + // return user?.uid; + } + + @override + Future currentUser() async { + // final FirebaseUser user = await _firebaseAuth.currentUser(); + // return user?.uid; + } + + @override + Future signOut() async { + // return _firebaseAuth.signOut(); + } +} \ No newline at end of file diff --git a/lib/FlutterUnitTesting/auth_provider.dart b/lib/FlutterUnitTesting/auth_provider.dart new file mode 100644 index 0000000..680e96f --- /dev/null +++ b/lib/FlutterUnitTesting/auth_provider.dart @@ -0,0 +1,16 @@ + +import 'package:flutter/material.dart'; + +import 'auth.dart'; + +class AuthProvider extends InheritedWidget { + const AuthProvider({Key key, Widget child, this.auth}) : super(key: key, child: child); + final BaseAuth auth; + + @override + bool updateShouldNotify(InheritedWidget oldWidget) => true; + + static AuthProvider of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } +} \ No newline at end of file diff --git a/lib/FlutterUnitTesting/home_page.dart b/lib/FlutterUnitTesting/home_page.dart new file mode 100644 index 0000000..311e72e --- /dev/null +++ b/lib/FlutterUnitTesting/home_page.dart @@ -0,0 +1,38 @@ + +import 'package:flutter/material.dart'; + +import 'auth.dart'; +import 'auth_provider.dart'; + +class HomePage extends StatelessWidget { + const HomePage({this.onSignedOut}); + final VoidCallback onSignedOut; + + Future _signOut(BuildContext context) async { + try { + final BaseAuth auth = AuthProvider.of(context).auth; + await auth.signOut(); + onSignedOut(); + } catch (e) { + print(e); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Welcome'), + actions: [ + FlatButton( + child: Text('Logout', style: TextStyle(fontSize: 17.0, color: Colors.white)), + onPressed: () => _signOut(context), + ) + ], + ), + body: Container( + child: Center(child: Text('Welcome', style: TextStyle(fontSize: 32.0))), + ), + ); + } +} \ No newline at end of file diff --git a/lib/FlutterUnitTesting/login_page.dart b/lib/FlutterUnitTesting/login_page.dart new file mode 100644 index 0000000..fad2c6b --- /dev/null +++ b/lib/FlutterUnitTesting/login_page.dart @@ -0,0 +1,144 @@ + + +import 'package:flutter/material.dart'; + +import 'auth.dart'; +import 'auth_provider.dart'; + +class EmailFieldValidator { + static String validate(String value) { + return value.isEmpty ? 'Email can\'t be empty' : null; + } +} + +class PasswordFieldValidator { + static String validate(String value) { + return value.isEmpty ? 'Password can\'t be empty' : null; + } +} + +class LoginPage extends StatefulWidget { + const LoginPage({this.onSignedIn}); + final VoidCallback onSignedIn; + + @override + State createState() => _LoginPageState(); +} + +enum FormType { + login, + register, +} + +class _LoginPageState extends State { + final GlobalKey formKey = GlobalKey(); + + String _email; + String _password; + FormType _formType = FormType.login; + + bool validateAndSave() { + final FormState form = formKey.currentState; + if (form.validate()) { + form.save(); + return true; + } + return false; + } + + Future validateAndSubmit() async { + if (validateAndSave()) { + try { + final BaseAuth auth = AuthProvider.of(context).auth; + if (_formType == FormType.login) { + final String userId = await auth.signInWithEmailAndPassword(_email, _password); + print('Signed in: $userId'); + } else { + final String userId = await auth.createUserWithEmailAndPassword(_email, _password); + print('Registered user: $userId'); + } + widget.onSignedIn(); + } catch (e) { + print('Error: $e'); + } + } + } + + void moveToRegister() { + formKey.currentState.reset(); + setState(() { + _formType = FormType.register; + }); + } + + void moveToLogin() { + formKey.currentState.reset(); + setState(() { + _formType = FormType.login; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Flutter login demo'), + ), + body: Container( + padding: EdgeInsets.all(16.0), + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: buildInputs() + buildSubmitButtons(), + ), + ), + ), + ); + } + + List buildInputs() { + return [ + TextFormField( + key: Key('email'), + decoration: InputDecoration(labelText: 'Email'), + validator: EmailFieldValidator.validate, + onSaved: (String value) => _email = value, + ), + TextFormField( + key: Key('password'), + decoration: InputDecoration(labelText: 'Password'), + obscureText: true, + validator: PasswordFieldValidator.validate, + onSaved: (String value) => _password = value, + ), + ]; + } + + List buildSubmitButtons() { + if (_formType == FormType.login) { + return [ + RaisedButton( + key: Key('signIn'), + child: Text('Login', style: TextStyle(fontSize: 20.0)), + onPressed: validateAndSubmit, + ), + FlatButton( + child: Text('Create an account', style: TextStyle(fontSize: 20.0)), + onPressed: moveToRegister, + ), + ]; + } else { + return [ + RaisedButton( + child: Text('Create an account', style: TextStyle(fontSize: 20.0)), + onPressed: validateAndSubmit, + ), + FlatButton( + child: Text('Have an account? Login', style: TextStyle(fontSize: 20.0)), + onPressed: moveToLogin, + ), + ]; + } + } +} \ No newline at end of file diff --git a/lib/FlutterUnitTesting/main_page.dart b/lib/FlutterUnitTesting/main_page.dart new file mode 100644 index 0000000..19aa19a --- /dev/null +++ b/lib/FlutterUnitTesting/main_page.dart @@ -0,0 +1,23 @@ + + +import 'package:flutter/material.dart'; + +import 'auth.dart'; +import 'auth_provider.dart'; +import 'root_page.dart'; + +class FlutterUnitTestApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AuthProvider( + auth: Auth(), + child: MaterialApp( + title: 'Flutter login demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: RootPage(), + ), + ); + } +} \ No newline at end of file diff --git a/lib/FlutterUnitTesting/root_page.dart b/lib/FlutterUnitTesting/root_page.dart new file mode 100644 index 0000000..59ba800 --- /dev/null +++ b/lib/FlutterUnitTesting/root_page.dart @@ -0,0 +1,72 @@ + + +import 'package:flutter/material.dart'; + +import 'auth.dart'; +import 'auth_provider.dart'; +import 'home_page.dart'; +import 'login_page.dart'; + +class RootPage extends StatefulWidget { + @override + State createState() => _RootPageState(); +} + +enum AuthStatus { + notDetermined, + notSignedIn, + signedIn, +} + +class _RootPageState extends State { + AuthStatus authStatus = AuthStatus.notDetermined; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final BaseAuth auth = AuthProvider.of(context).auth; + auth.currentUser().then((String userId) { + setState(() { + authStatus = userId == null ? AuthStatus.notSignedIn : AuthStatus.signedIn; + }); + }); + } + + void _signedIn() { + setState(() { + authStatus = AuthStatus.signedIn; + }); + } + + void _signedOut() { + setState(() { + authStatus = AuthStatus.notSignedIn; + }); + } + + @override + Widget build(BuildContext context) { + switch (authStatus) { + case AuthStatus.notDetermined: + return _buildWaitingScreen(); + case AuthStatus.notSignedIn: + return LoginPage( + onSignedIn: _signedIn, + ); + case AuthStatus.signedIn: + return HomePage( + onSignedOut: _signedOut, + ); + } + return null; + } + + Widget _buildWaitingScreen() { + return Scaffold( + body: Container( + alignment: Alignment.center, + child: CircularProgressIndicator(), + ), + ); + } +} \ No newline at end of file diff --git a/lib/FlutterWeb/datamodels/episode_item_model.dart b/lib/FlutterWeb/datamodels/episode_item_model.dart new file mode 100644 index 0000000..397adab --- /dev/null +++ b/lib/FlutterWeb/datamodels/episode_item_model.dart @@ -0,0 +1,16 @@ +class EpisodeItemModel { + final String title; + final double duration; + final String imageUrl; + + EpisodeItemModel({ + this.title, + this.duration, + this.imageUrl, + }); + + EpisodeItemModel.fromJson(Map map) + : title = map['title'], + duration = map['duration'], + imageUrl = map['imageUrl']; +} diff --git a/lib/FlutterWeb/datamodels/navbar_item_model.dart b/lib/FlutterWeb/datamodels/navbar_item_model.dart new file mode 100644 index 0000000..c3b4d63 --- /dev/null +++ b/lib/FlutterWeb/datamodels/navbar_item_model.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; + +class NavBarItemModel { + final String title; + final String navigationPath; + final IconData iconData; + + NavBarItemModel({ + this.title, + this.navigationPath, + this.iconData, + }); +} diff --git a/lib/FlutterWeb/datamodels/season_details_model.dart b/lib/FlutterWeb/datamodels/season_details_model.dart new file mode 100644 index 0000000..5bb623f --- /dev/null +++ b/lib/FlutterWeb/datamodels/season_details_model.dart @@ -0,0 +1,11 @@ +import 'package:flutter/foundation.dart'; + +class SeasonDetailsModel { + final String title; + final String description; + + SeasonDetailsModel({ + @required this.title, + @required this.description, + }); +} diff --git a/lib/FlutterWeb/locator.dart b/lib/FlutterWeb/locator.dart index 82c3bc8..77ceaa7 100644 --- a/lib/FlutterWeb/locator.dart +++ b/lib/FlutterWeb/locator.dart @@ -1,4 +1,5 @@ +import 'package:all_flutter_gives/FlutterWeb/services/api.dart'; import 'package:all_flutter_gives/FlutterWeb/services/navigation_service.dart'; import 'package:get_it/get_it.dart'; @@ -6,4 +7,5 @@ GetIt locator = GetIt.instance; void setupLocator() { locator.registerLazySingleton(() => NavigationService()); + locator.registerLazySingleton(() => ApiService()); } \ No newline at end of file diff --git a/lib/FlutterWeb/routing/route_names.dart b/lib/FlutterWeb/routing/route_names.dart index c33e0e3..2e33c07 100644 --- a/lib/FlutterWeb/routing/route_names.dart +++ b/lib/FlutterWeb/routing/route_names.dart @@ -1,4 +1,4 @@ -const String HomeRoute = "home"; -const String AboutRoute = "about"; -const String EpisodesRoute = "episodes"; \ No newline at end of file +const String HomeRoute = "/home"; +const String AboutRoute = "/about"; +const String EpisodesRoute = "/episodes"; \ No newline at end of file diff --git a/lib/FlutterWeb/services/api.dart b/lib/FlutterWeb/services/api.dart new file mode 100644 index 0000000..ba11490 --- /dev/null +++ b/lib/FlutterWeb/services/api.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; + +import 'package:all_flutter_gives/FlutterWeb/datamodels/episode_item_model.dart'; +import 'package:http/http.dart' as http; + +class ApiService { + // @ HTTP Request level + // 1. Make your HTTP request + // 2. Get the formatted data back + // 3. Serialize it and pass it down to your application and let your app decide what to do with the data. + // You can pass it down to either BLOC file or ViewModel + + static const String _apiEndpoint = + 'https://us-central1-thebasics-2f123.cloudfunctions.net/thebasics/'; + + Future getEpisodes() async { + var response = await http.get(Uri.parse('$_apiEndpoint/courseEpisodes')); + + if (response.statusCode == 200) { + var episodes = ((jsonDecode(response.body)) as List) + .map((episode) => EpisodeItemModel.fromJson(episode)) + .toList(); + + return episodes; + } + + return 'Counld not fetch results at this time'; + } +} diff --git a/lib/FlutterWeb/styles/text_styles.dart b/lib/FlutterWeb/styles/text_styles.dart new file mode 100644 index 0000000..de84920 --- /dev/null +++ b/lib/FlutterWeb/styles/text_styles.dart @@ -0,0 +1,20 @@ +import 'package:flutter/widgets.dart'; +import 'package:responsive_builder/responsive_builder.dart'; + +/// Returns the style for a page title based on the [deviceScreenType] passed in. +TextStyle titleTextStyle(DeviceScreenType deviceScreenType) { + double titleSize = deviceScreenType == DeviceScreenType.mobile ? 50 : 80; + return TextStyle( + fontWeight: FontWeight.w800, height: 0.9, fontSize: titleSize); +} + +/// Return the style for description text on a page based on the [deviceScreenType] passed in. +TextStyle descriptionTextStyle(DeviceScreenType deviceScreenType) { + double descriptionSize = + deviceScreenType == DeviceScreenType.mobile ? 16 : 21; + + return TextStyle( + fontSize: descriptionSize, + height: 1.7, + ); +} diff --git a/lib/FlutterWeb/viewmodels/episodes_view_model.dart b/lib/FlutterWeb/viewmodels/episodes_view_model.dart new file mode 100644 index 0000000..53fa4d1 --- /dev/null +++ b/lib/FlutterWeb/viewmodels/episodes_view_model.dart @@ -0,0 +1,24 @@ +import 'package:all_flutter_gives/FlutterWeb/datamodels/episode_item_model.dart'; +import 'package:all_flutter_gives/FlutterWeb/services/api.dart'; +import 'package:flutter/material.dart'; + +import '../locator.dart'; + +class EpisodesViewModel extends ChangeNotifier { + final _api = locator(); + + List _episodes; + + List get episodes => _episodes; + + Future getEpisodes() async { + var episodesResults = await _api.getEpisodes(); + + if (episodesResults is String) + _episodes = []; + else + _episodes = episodesResults; + + notifyListeners(); + } +} diff --git a/lib/FlutterWeb/views/episodes/episodes_view.dart b/lib/FlutterWeb/views/episodes/episodes_view.dart index 99d8fae..574d7f7 100644 --- a/lib/FlutterWeb/views/episodes/episodes_view.dart +++ b/lib/FlutterWeb/views/episodes/episodes_view.dart @@ -1,12 +1,43 @@ +import 'package:all_flutter_gives/FlutterWeb/datamodels/season_details_model.dart'; +import 'package:all_flutter_gives/FlutterWeb/viewmodels/episodes_view_model.dart'; +import 'package:all_flutter_gives/FlutterWeb/widgets/episodes_list/episodes_list.dart'; +import 'package:all_flutter_gives/FlutterWeb/widgets/season_details/season_details.dart'; import 'package:flutter/material.dart'; +import 'package:provider_architecture/_viewmodel_provider.dart'; class FlutterWebEpisodesScreen extends StatelessWidget { const FlutterWebEpisodesScreen({Key key}) : super(key: key); @override Widget build(BuildContext context) { - return Container( - child: Text('Episodes'), + return ViewModelProvider.withConsumer( + viewModelBuilder: () => EpisodesViewModel(), + onModelReady: (model) => model.getEpisodes(), + builder: (context, model, child) => SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + SizedBox( + height: 100, + ), + SeasonDetails( + details: SeasonDetailsModel( + title: 'SEASON 1', + description: + 'This season covers the absolute basics of Flutter Web Dev to get us up and running with a basic web app.', + ), + ), + SizedBox( + height: 50, + ), + model.episodes == null + ? CircularProgressIndicator() + : EpisodesList( + episodes: model.episodes, + ), + ], + )), ); } } diff --git a/lib/FlutterWeb/views/layout_template/layout_template.dart b/lib/FlutterWeb/views/layout_template/layout_template.dart index 1e81bfd..d67b33d 100644 --- a/lib/FlutterWeb/views/layout_template/layout_template.dart +++ b/lib/FlutterWeb/views/layout_template/layout_template.dart @@ -12,7 +12,8 @@ import 'package:responsive_builder/responsive_builder.dart'; import '../../locator.dart'; class FlutterWebLayoutTemplate extends StatelessWidget { - const FlutterWebLayoutTemplate({Key key}) : super(key: key); + final Widget child; + const FlutterWebLayoutTemplate({Key key, @required this.child}) : super(key: key); // This layout template is for easy navigation between pages by using the Navigator Widget. @@ -27,11 +28,7 @@ class FlutterWebLayoutTemplate extends StatelessWidget { children: [ NavigationBar(), Expanded( - child: Navigator( - key: locator().navigatorKey, - onGenerateRoute: generateRoute, - initialRoute: HomeRoute, - ), + child: child, ) ], ), diff --git a/lib/FlutterWeb/widgets/episodes_list/episode_item.dart b/lib/FlutterWeb/widgets/episodes_list/episode_item.dart new file mode 100644 index 0000000..b898572 --- /dev/null +++ b/lib/FlutterWeb/widgets/episodes_list/episode_item.dart @@ -0,0 +1,53 @@ +import 'package:all_flutter_gives/FlutterWeb/datamodels/episode_item_model.dart'; +import 'package:flutter/material.dart'; + +class EpisodeItem extends StatelessWidget { + final EpisodeItemModel model; + const EpisodeItem({ + Key key, + this.model, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Card( + color: Colors.white, + elevation: 2, + child: SizedBox( + width: 360, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 180, + child: Image.network(model.imageUrl, fit: BoxFit.cover,), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15.0, + vertical: 20, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + model.title, + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14, + ), + softWrap: true, + ), + Text( + '${model.duration} minutes', + style: TextStyle(fontSize: 10), + ) + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/FlutterWeb/widgets/episodes_list/episodes_list.dart b/lib/FlutterWeb/widgets/episodes_list/episodes_list.dart new file mode 100644 index 0000000..2dddb52 --- /dev/null +++ b/lib/FlutterWeb/widgets/episodes_list/episodes_list.dart @@ -0,0 +1,24 @@ +import 'package:all_flutter_gives/FlutterWeb/datamodels/episode_item_model.dart'; +import 'package:flutter/material.dart'; + +import 'episode_item.dart'; + +class EpisodesList extends StatelessWidget { + + final List episodes; + EpisodesList({this.episodes}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 30, + runSpacing: 30, + children: [ + ...episodes.map( + (episode) => EpisodeItem(model: episode), + ) + ], + ); + } + +} diff --git a/lib/FlutterWeb/widgets/navbar_item/navbar_item_desktop.dart b/lib/FlutterWeb/widgets/navbar_item/navbar_item_desktop.dart new file mode 100644 index 0000000..2b62e30 --- /dev/null +++ b/lib/FlutterWeb/widgets/navbar_item/navbar_item_desktop.dart @@ -0,0 +1,19 @@ + + +import 'package:all_flutter_gives/FlutterWeb/datamodels/navbar_item_model.dart'; +import 'package:flutter/material.dart'; +import 'package:provider_architecture/provider_architecture.dart'; + +class NavBarItemTabletDesktop extends ProviderWidget { + + @override + Widget build( + BuildContext context, + NavBarItemModel model + ) { + return Text( + model.title, + style: TextStyle(fontSize: 18), + ); + } +} \ No newline at end of file diff --git a/lib/FlutterWeb/widgets/navbar_item/navbar_item_mobile.dart b/lib/FlutterWeb/widgets/navbar_item/navbar_item_mobile.dart new file mode 100644 index 0000000..edfa16f --- /dev/null +++ b/lib/FlutterWeb/widgets/navbar_item/navbar_item_mobile.dart @@ -0,0 +1,28 @@ + + +import 'package:all_flutter_gives/FlutterWeb/datamodels/navbar_item_model.dart'; +import 'package:all_flutter_gives/FlutterWeb/widgets/navbar_item/navbar_items.dart'; +import 'package:flutter/material.dart'; +import 'package:provider_architecture/_provider_widget.dart'; + +class NavBarItemMobile extends ProviderWidget { + + @override + Widget build(BuildContext context, NavBarItemModel model) { + return Padding( + padding: const EdgeInsets.only(left: 30, top: 60), + child: Row( + children: [ + Icon(model.iconData), + SizedBox( + width: 30, + ), + Text( + model.title, + style: TextStyle(fontSize: 18), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/FlutterWeb/widgets/navbar_item/navbar_items.dart b/lib/FlutterWeb/widgets/navbar_item/navbar_items.dart new file mode 100644 index 0000000..3c39164 --- /dev/null +++ b/lib/FlutterWeb/widgets/navbar_item/navbar_items.dart @@ -0,0 +1,39 @@ +import 'package:all_flutter_gives/FlutterWeb/datamodels/navbar_item_model.dart'; +import 'package:all_flutter_gives/FlutterWeb/services/navigation_service.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:responsive_builder/responsive_builder.dart'; + +import '../../locator.dart'; +import 'navbar_item_desktop.dart'; +import 'navbar_item_mobile.dart'; + +class NavBarItem extends StatelessWidget { + final String title; + final String navigationPath; + final IconData icon; + const NavBarItem(this.title, this.navigationPath, {this.icon}); + + @override + Widget build(BuildContext context) { + var model = NavBarItemModel( + title: title, + navigationPath: navigationPath, + iconData: icon, + ); + return GestureDetector( + onTap: () { + // DON'T EVER USE A SERVICE DIRECTLY IN THE UI TO CHANGE ANY KIND OF STATE + // SERVICES SHOULD ONLY BE USED FROM A VIEWMODEL + locator().navigateTo(navigationPath); + }, + child: Provider.value( + value: model, + child: ScreenTypeLayout( + tablet: NavBarItemTabletDesktop(), + mobile: NavBarItemMobile(), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/FlutterWeb/widgets/navigation_bar/navbar_items.dart b/lib/FlutterWeb/widgets/navigation_bar/navbar_items.dart deleted file mode 100644 index a7fcdb5..0000000 --- a/lib/FlutterWeb/widgets/navigation_bar/navbar_items.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:all_flutter_gives/FlutterWeb/services/navigation_service.dart'; -import 'package:flutter/material.dart'; - -import '../../locator.dart'; - -Widget navigationBarItems(String title, String navigationPath) => GestureDetector( - onTap: () { - locator().navigateTo(navigationPath); - }, - child: Text( - title, - style: TextStyle(fontSize: 18), - ), -); diff --git a/lib/FlutterWeb/widgets/navigation_bar/navigation_bar.dart b/lib/FlutterWeb/widgets/navigation_bar/navigation_bar.dart index 26471d5..27929e3 100644 --- a/lib/FlutterWeb/widgets/navigation_bar/navigation_bar.dart +++ b/lib/FlutterWeb/widgets/navigation_bar/navigation_bar.dart @@ -3,7 +3,7 @@ import 'package:all_flutter_gives/FlutterWeb/widgets/navigation_bar/navigation_b import 'package:flutter/material.dart'; import 'package:responsive_builder/responsive_builder.dart'; -import 'navbar_items.dart'; +import '../navbar_item/navbar_items.dart'; import 'navigation_bar_mobile.dart'; class NavigationBar extends StatelessWidget { diff --git a/lib/FlutterWeb/widgets/navigation_bar/navigation_bar_tablet_desktop.dart b/lib/FlutterWeb/widgets/navigation_bar/navigation_bar_tablet_desktop.dart index d740a1f..a7b4d73 100644 --- a/lib/FlutterWeb/widgets/navigation_bar/navigation_bar_tablet_desktop.dart +++ b/lib/FlutterWeb/widgets/navigation_bar/navigation_bar_tablet_desktop.dart @@ -2,7 +2,7 @@ import 'package:all_flutter_gives/FlutterWeb/routing/route_names.dart'; import 'package:flutter/material.dart'; -import 'navbar_items.dart'; +import '../navbar_item/navbar_items.dart'; import 'navbar_logo.dart'; class NavigationBarTabletDesktop extends StatelessWidget { @@ -18,11 +18,11 @@ class NavigationBarTabletDesktop extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - navigationBarItems('Episodes', EpisodesRoute), + NavBarItem('Episodes', EpisodesRoute), SizedBox( width: 60, ), - navigationBarItems('About', AboutRoute) + NavBarItem('About', AboutRoute) ], ) ], diff --git a/lib/FlutterWeb/widgets/navigation_drawer/drawer_item.dart b/lib/FlutterWeb/widgets/navigation_drawer/drawer_item.dart index 8ed59d9..25324b9 100644 --- a/lib/FlutterWeb/widgets/navigation_drawer/drawer_item.dart +++ b/lib/FlutterWeb/widgets/navigation_drawer/drawer_item.dart @@ -1,5 +1,5 @@ -import 'package:all_flutter_gives/FlutterWeb/widgets/navigation_bar/navbar_items.dart'; +import 'package:all_flutter_gives/FlutterWeb/widgets/navbar_item/navbar_items.dart'; import 'package:flutter/material.dart'; class DrawerItem extends StatelessWidget { @@ -16,7 +16,7 @@ class DrawerItem extends StatelessWidget { children: [ Icon(icon), SizedBox(width: 30), - navigationBarItems(title, navigationPath), + NavBarItem(title, navigationPath), ], ), ); diff --git a/lib/FlutterWeb/widgets/season_details/season_details.dart b/lib/FlutterWeb/widgets/season_details/season_details.dart new file mode 100644 index 0000000..bac79f9 --- /dev/null +++ b/lib/FlutterWeb/widgets/season_details/season_details.dart @@ -0,0 +1,22 @@ +import 'package:all_flutter_gives/FlutterWeb/datamodels/season_details_model.dart'; +import 'package:all_flutter_gives/FlutterWeb/widgets/season_details/season_details_desktop.dart'; +import 'package:all_flutter_gives/FlutterWeb/widgets/season_details/season_details_mobile.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:responsive_builder/responsive_builder.dart'; + +class SeasonDetails extends StatelessWidget { + final SeasonDetailsModel details; + const SeasonDetails({Key key, this.details}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Provider.value( + value: details, + child: ScreenTypeLayout( + desktop: SeasonDetailsDesktop(), + mobile: SeasonDetailsMobile(), + ), + ); + } +} diff --git a/lib/FlutterWeb/widgets/season_details/season_details_desktop.dart b/lib/FlutterWeb/widgets/season_details/season_details_desktop.dart new file mode 100644 index 0000000..1996f55 --- /dev/null +++ b/lib/FlutterWeb/widgets/season_details/season_details_desktop.dart @@ -0,0 +1,33 @@ +import 'package:all_flutter_gives/FlutterWeb/datamodels/season_details_model.dart'; +import 'package:all_flutter_gives/FlutterWeb/styles/text_styles.dart'; +import 'package:flutter/material.dart'; +import 'package:provider_architecture/provider_architecture.dart'; +import 'package:responsive_builder/responsive_builder.dart'; + +class SeasonDetailsDesktop extends ProviderWidget { + + @override + Widget build(BuildContext context, SeasonDetailsModel details) { + return ResponsiveBuilder( + builder: (context, sizingInformation) => Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + details.title, + style: titleTextStyle(sizingInformation.deviceScreenType), + ), + SizedBox( + width: 50, + ), + Expanded( + child: Text( + details.description, + style: descriptionTextStyle(sizingInformation.deviceScreenType), + ), + ) + ], + ), + ); + } +} diff --git a/lib/FlutterWeb/widgets/season_details/season_details_mobile.dart b/lib/FlutterWeb/widgets/season_details/season_details_mobile.dart new file mode 100644 index 0000000..629955c --- /dev/null +++ b/lib/FlutterWeb/widgets/season_details/season_details_mobile.dart @@ -0,0 +1,29 @@ +import 'package:all_flutter_gives/FlutterWeb/datamodels/season_details_model.dart'; +import 'package:all_flutter_gives/FlutterWeb/styles/text_styles.dart'; +import 'package:flutter/material.dart'; +import 'package:provider_architecture/provider_architecture.dart'; +import 'package:responsive_builder/responsive_builder.dart'; + +class SeasonDetailsMobile extends ProviderWidget { + + @override + Widget build(BuildContext context, SeasonDetailsModel details) { + return ResponsiveBuilder( + builder: (context, sizingInformation) => Column( + children: [ + Text( + details.title, + style: titleTextStyle(sizingInformation.deviceScreenType), + ), + SizedBox( + height: 50, + ), + Text( + details.description, + style: descriptionTextStyle(sizingInformation.deviceScreenType), + ), + ], + ), + ); + } +} diff --git a/lib/FoodAppUI/main.dart b/lib/FoodAppUI/main.dart new file mode 100644 index 0000000..06ecca7 --- /dev/null +++ b/lib/FoodAppUI/main.dart @@ -0,0 +1,26 @@ +import 'package:all_flutter_gives/FoodAppUI/screens/landing_screen.dart'; +import 'package:all_flutter_gives/FoodAppUI/utils/constants.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +//Based on this Youtube tutorial +// https://www.youtube.com/watch?v=SYtlmeznJhk + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(statusBarColor: COLOR_GREEN)); + return LayoutBuilder(builder: (context, constraints) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Screen 2', + theme: ThemeData(textTheme: defaultText), + home: LandingScreen(), + ); + }); + } +} \ No newline at end of file diff --git a/lib/FoodAppUI/screens/landing_screen.dart b/lib/FoodAppUI/screens/landing_screen.dart new file mode 100644 index 0000000..0946b4a --- /dev/null +++ b/lib/FoodAppUI/screens/landing_screen.dart @@ -0,0 +1,219 @@ +import 'package:all_flutter_gives/FoodAppUI/screens/product_item.dart'; +import 'package:all_flutter_gives/FoodAppUI/screens/product_page.dart'; +import 'package:all_flutter_gives/FoodAppUI/utils/constants.dart'; +import 'package:all_flutter_gives/FoodAppUI/utils/widget_functions.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +const PRODUCT_DATA = [ + {"image": "pizza.png", "name": "Pizza", "rest": "Maritine Star Restaurant", "rating": "4.5 (164)", "price": 20, "currency": "\$"}, + {"image": "burger.png", "name": "Burger", "rest": "Maritine Star Restaurant", "rating": "4.7 (199)", "price": 10, "currency": "\$"}, + {"image": "fries.png", "name": "Fries", "rest": "Maritine Star Restaurant", "rating": "4.2 (101)", "price": 10, "currency": "\$"}, + {"image": "hotdog.png", "name": "HotDog", "rest": "Maritine Star Restaurant", "rating": "3.9 (150)", "price": 15, "currency": "\$"}, +]; + +const CATEGORIES = [ + {"image": "salad.png", "name": "Salad"}, + {"image": "fastfood.png", "name": "Fast Food"}, + {"image": "desert.png", "name": "Desert"}, + {"image": "drinks.png", "name": "Drinks"}, + {"image": "drinks.png", "name": "Drinks"}, +]; + +class LandingScreen extends StatefulWidget { + const LandingScreen({Key key}) : super(key: key); + + @override + _LandingScreenState createState() => _LandingScreenState(); +} + +class _LandingScreenState extends State { + final FocusNode _focusNode = FocusNode(); + + @override + void dispose() { + super.dispose(); + _focusNode.unfocus(); + } + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: LayoutBuilder( + builder: (context, constraints) { + return Container( + child: Column( + children: [ + Expanded( + flex: 4, + child: Stack( + fit: StackFit.expand, + children: [ + Container( + color: COLOR_GREY, + ), + Image.asset("assets/images/tree_v.png"), + Padding( + padding: const EdgeInsets.all(10), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 70, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.0), + color: Colors.white, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Image.asset("assets/images/avatar.png"), + )), + addHorizontalSpace(20), + Expanded( + flex: 7, + child: Text( + "How Hungry are you Today?", + style: textTheme.headline5.apply(color: Colors.white), + ), + ) + ], + ), + TextField( + focusNode: _focusNode, + cursorColor: Colors.white, + cursorRadius: Radius.circular(10.0), + style: TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: "Search Food Items", + hintStyle: TextStyle(color: Colors.white), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(20.0), borderSide: BorderSide.none), + prefixIcon: Icon( + Icons.search, + color: Colors.white, + ), + suffixIcon: Material( + color: Colors.transparent, + child: ClipRect( + child: InkWell( + highlightColor: Colors.orange, + onTap: (){print("You tapped me");}, + child: Container( + height: 70, + width: 70, + decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.only(topRight: Radius.circular(20.0), bottomRight: Radius.circular(20.0))), + child: Icon( + Icons.menu, + color: Colors.white, + ), + ), + ), + ), + ), + filled: true, + fillColor: Colors.white24), + ), + addVerticalSpace(10), + ], + ), + ) + ], + )), + Container( + width: constraints.maxWidth, + color: Colors.grey.shade200, + child: Padding( + padding: const EdgeInsets.only(left: 10.0, bottom: 10.0), + child: Stack( + clipBehavior: Clip.none, // todo : This is new to me and interesting too + children: [ + Column( + children: [ + addVerticalSpace(constraints.maxWidth * 0.35), + Row( + children: [ + Text( + "Popular Foods", + style: textTheme.headline5, + ), + Expanded( + child: Center(), + ), + Text( + "View All > ", + style: textTheme.subtitle2.apply(color: COLOR_ORANGE), + ), + addHorizontalSpace(10), + ], + ), + addVerticalSpace(10), + SingleChildScrollView( // todo : This is new to me and interesting too. Using SingleChildScrollView instead of ListView + scrollDirection: Axis.horizontal, + physics: BouncingScrollPhysics(), + child: Row( + children: PRODUCT_DATA + .map((data) => InkWell( + onTap: () { + _focusNode.unfocus(); + Navigator.of(context).push(MaterialPageRoute(builder: (context) => ProductPage(productData: data))); + }, + child: ProductItem( + width: constraints.maxWidth * 0.50, + productData: data, + ), + )) + .toList(), + ), + ), + ], + ), + Positioned( + top: -40, + left: 0, + child: Container( + width: constraints.maxWidth, + height: constraints.maxWidth * 0.35, + child: ListView( + scrollDirection: Axis.horizontal, + physics: BouncingScrollPhysics(), + children: CATEGORIES + .map((category) => Container( + margin: const EdgeInsets.only(right: 10.0), + width: constraints.maxWidth * 0.25, + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.0)), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + children: [ + Image.asset("assets/images/${category['image']}"), + addVerticalSpace(10), + Text( + "${category['name']}", + style: textTheme.bodyText2.apply(color: COLOR_BLACK), + ) + ], + ), + ), + )) + .toList(), + ), + ), + ), + ], + ), + ), + ) + ], + ), + ); + }, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/FoodAppUI/screens/product_item.dart b/lib/FoodAppUI/screens/product_item.dart new file mode 100644 index 0000000..88feec4 --- /dev/null +++ b/lib/FoodAppUI/screens/product_item.dart @@ -0,0 +1,91 @@ +import 'package:all_flutter_gives/FoodAppUI/utils/constants.dart'; +import 'package:all_flutter_gives/FoodAppUI/utils/widget_functions.dart'; +import 'package:flutter/material.dart'; + +class ProductItem extends StatelessWidget { + final double width; + final dynamic productData; + + const ProductItem({Key key, this.width, this.productData}) : super(key: key); + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final height = width * 4 / 3; + return Container( + margin: const EdgeInsets.only(right: 20), + width: width, + height: height + 40, + child: Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded(flex: 2, child: Container()), + Expanded( + flex: 7, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.0), + color: Colors.white, + ), + ), + ) + ], + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Image.asset( + "assets/images/${productData['image']}", + width: width * 0.80, + )), + Expanded(child: Center()), + Text( + "${productData['name']}", + style: textTheme.headline6, + ), + addVerticalSpace(5), + RichText( + overflow: TextOverflow.ellipsis, + text: TextSpan(children: [ // Todo : This is a cool way of using TextSpan by having an icon inside it. + WidgetSpan(child: Icon(Icons.location_on, color: Colors.red, size: 15)), + TextSpan(text: "${productData['rest']}", style: textTheme.caption) + ])), + addVerticalSpace(15), + Row( + children: [ + Expanded( + flex: 5, + child: RichText( + textAlign: TextAlign.left, + overflow: TextOverflow.ellipsis, + text: TextSpan(children: [ + WidgetSpan(child: Icon(Icons.star, color: Colors.orange, size: 15)), + TextSpan(text: "${productData['rating']}", style: textTheme.subtitle2.apply(fontWeightDelta: 4)) + ])), + ), + Expanded( + flex: 5, + child: RichText( + textAlign: TextAlign.right, + overflow: TextOverflow.ellipsis, + text: TextSpan(children: [ + TextSpan(text: "\$", style: TextStyle(color: COLOR_ORANGE)), + TextSpan(text: "${productData['price']}", style: textTheme.headline5.apply(color: COLOR_ORANGE)) + ])), + ), + ], + ) + ], + ), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/FoodAppUI/screens/product_page.dart b/lib/FoodAppUI/screens/product_page.dart new file mode 100644 index 0000000..f8e1827 --- /dev/null +++ b/lib/FoodAppUI/screens/product_page.dart @@ -0,0 +1,216 @@ +import 'package:all_flutter_gives/FoodAppUI/utils/buttons.dart'; +import 'package:all_flutter_gives/FoodAppUI/utils/constants.dart'; +import 'package:all_flutter_gives/FoodAppUI/utils/widget_functions.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:slide_to_act/slide_to_act.dart'; + +class ProductPage extends StatefulWidget { + final Map productData; + + const ProductPage({Key key, this.productData}) : super(key: key); + + @override + _ProductPageState createState() => _ProductPageState(); +} + +class _ProductPageState extends State { + final GlobalKey _buttonKey = GlobalKey(); + bool addedToCart = false; // Just for Demonstration + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + return SafeArea( + child: Scaffold( + body: LayoutBuilder(builder: (context, constraints) { + return Container( + height: constraints.maxHeight, + child: Stack( + children: [ + SingleChildScrollView( + child: Container( + child: Column( + children: [ + Container( + height: constraints.maxHeight * 0.40, + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + color: Color(0xffE2F3D4), + ), + child: Center( + child: Image.asset( + "assets/images/${widget.productData['image']}", + width: constraints.maxWidth * 0.50, + ), + ), + ), + Positioned( + top: 10, + left: 10, + child: SquareIconButton( + icon: Icons.arrow_back_ios_outlined, + width: 50, + onPressed: () { + Navigator.of(context).pop(); + }, + buttonColor: Colors.orange.shade100, + iconColor: Colors.orange, + )) + ], + ), + ), + addVerticalSpace(10), + Container( + color: Colors.grey.shade50, + child: Stack( + clipBehavior: Clip.none, + children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + addVerticalSpace(20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${widget.productData['name']}", + style: textTheme.headline5, + ), + addVerticalSpace(5), + RichText( + overflow: TextOverflow.ellipsis, + text: TextSpan(children: [ + WidgetSpan(child: Icon(Icons.location_on, color: Colors.red, size: 15)), + TextSpan(text: "${widget.productData['rest']}", style: textTheme.subtitle2.apply(color: COLOR_GREY)) + ])), + ], + ), + RichText( + textAlign: TextAlign.right, + overflow: TextOverflow.ellipsis, + text: TextSpan(children: [ + TextSpan(text: "\$", style: TextStyle(color: COLOR_ORANGE)), + TextSpan(text: "${widget.productData['price']}", style: textTheme.headline5.apply(color: COLOR_ORANGE)) + ])), + ], + ), + addVerticalSpace(20), + Divider(), + Container( + margin: const EdgeInsets.symmetric(vertical: 20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + RichText( + overflow: TextOverflow.ellipsis, + text: TextSpan(children: [ + WidgetSpan(child: Icon(Icons.star, color: Colors.orange, size: 15)), + TextSpan(text: "${widget.productData['rating']}", style: textTheme.bodyText2.apply(fontWeightDelta: 4)) + ])), + RichText( + overflow: TextOverflow.ellipsis, + text: TextSpan(children: [ + WidgetSpan(child: Icon(Icons.access_time_sharp, color: Colors.red, size: 15)), + TextSpan(text: " 18 Mins", style: textTheme.bodyText2.apply(fontWeightDelta: 4)) + ])), + RichText( + overflow: TextOverflow.ellipsis, + text: TextSpan(children: [ + WidgetSpan(child: Icon(Icons.location_on, color: Colors.green, size: 15)), + TextSpan(text: "2.3 KM" , style: textTheme.bodyText2.apply(fontWeightDelta: 4)) + ])), + ], + ), + ), + Divider(), + Text( + "Overview", + style: textTheme.headline6, + ), + addVerticalSpace(10), + Text( + "A pizza that decidedly staggers under an overload of golden corn, exotic black olives, crunchy onions, crisp capsicum, succulent mushrooms, juicyfresh tomatoes and jalapeno - with extra cheese to go all around. A pizza that goes ballistic on veggies! Check out this mouth watering overload of crunchy, crisp capsicum, succulent mushrooms and fresh tomatoes", + style: textTheme.subtitle2.apply(heightDelta: 2.0), + ), + addVerticalSpace(100), + ], + ), + ), + Positioned( + top: -35, + right: 20, + child: InkWell( + onTap: () { + Fluttertoast.showToast(msg: "Added to Favorite"); + }, + child: Container( + width: 70, + height: 70, + decoration: + BoxDecoration(shape: BoxShape.circle, color: Colors.red, boxShadow: [BoxShadow(color: Colors.red.withOpacity(0.2), blurRadius: 10.0, spreadRadius: 5.0)]), + child: Icon( + Icons.favorite, + color: Colors.white, + size: 35.0, + ), + ), + ), + ) + ], + ), + ), + ], + ), + ), + ), + if (!addedToCart) ...[ + Positioned( + bottom: 0, + left: 0, + child: Container( + width: constraints.maxWidth, + height: constraints.maxHeight * 0.12, + decoration: BoxDecoration(borderRadius: BorderRadius.only(topLeft: Radius.circular(20.0), topRight: Radius.circular(20.0)), color: Colors.white), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: SlideAction( + text: "Add to Cart", + key: _buttonKey, + sliderButtonIcon: Icon( + Icons.shopping_bag, + color: Colors.white, + ), + onSubmit: () { + Future.delayed(Duration(seconds: 1), () { + setState(() { + addedToCart = true; + }); + // _buttonKey.currentState!.reset(); + }); + }, + sliderRotate: false, + borderRadius: 10.0, + elevation: 0, + innerColor: COLOR_GREEN, + outerColor: Colors.grey.shade100, + ), + ), + ), + ) + ] + ], + ), + ); + }), + ), + ); + } +} \ No newline at end of file diff --git a/lib/FoodAppUI/utils/buttons.dart b/lib/FoodAppUI/utils/buttons.dart new file mode 100644 index 0000000..b3cb784 --- /dev/null +++ b/lib/FoodAppUI/utils/buttons.dart @@ -0,0 +1,39 @@ +import 'package:all_flutter_gives/FoodAppUI/utils/constants.dart'; +import 'package:flutter/material.dart'; + +class SquareIconButton extends StatelessWidget { + final Function() onPressed; + final Color iconColor, buttonColor; + final double width; + final IconData icon; + final double borderRadius; + + const SquareIconButton( + {Key key, + this.onPressed, + this.iconColor = COLOR_GREEN, + this.buttonColor = Colors.white, + this.width = 70, + this.icon, + this.borderRadius = 10}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onPressed, + child: Container( + width: width, + height: width, + child: Icon( + this.icon, + color: iconColor, + ), + decoration: BoxDecoration( + color: buttonColor, + borderRadius: BorderRadius.circular(borderRadius), + ), + ), + ); + } +} diff --git a/lib/FoodAppUI/utils/constants.dart b/lib/FoodAppUI/utils/constants.dart new file mode 100644 index 0000000..53210f8 --- /dev/null +++ b/lib/FoodAppUI/utils/constants.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +const COLOR_BLACK = Colors.black; +const COLOR_ORANGE = Colors.deepOrange; +const COLOR_GREY = Color(0xff9E9E9E); +const COLOR_WHITE = Color(0xffFFA801); +const COLOR_GREEN = Color(0xff7BB655); + +TextTheme defaultText = TextTheme( + headline1: GoogleFonts.nunito(fontWeight: FontWeight.bold, fontSize: 96), + headline2: GoogleFonts.nunito(fontWeight: FontWeight.bold, fontSize: 60), + headline3: GoogleFonts.nunito(fontWeight: FontWeight.bold, fontSize: 48), + headline4: GoogleFonts.nunito(fontWeight: FontWeight.bold, fontSize: 34), + headline5: GoogleFonts.nunito(fontWeight: FontWeight.bold, fontSize: 24), + headline6: GoogleFonts.nunito(fontWeight: FontWeight.bold, fontSize: 20), + bodyText1: GoogleFonts.nunito(fontSize: 16, fontWeight: FontWeight.normal), + bodyText2: GoogleFonts.nunito( + fontSize: 14, + fontWeight: FontWeight.normal, + ), + subtitle1: GoogleFonts.nunito(fontSize: 16, fontWeight: FontWeight.normal), + subtitle2: GoogleFonts.nunito(fontSize: 14, fontWeight: FontWeight.w400), + button: GoogleFonts.nunito(fontSize: 14, fontWeight: FontWeight.w400), + caption: GoogleFonts.nunito(fontSize: 12, fontWeight: FontWeight.normal)); \ No newline at end of file diff --git a/lib/FoodAppUI/utils/custom_functions.dart b/lib/FoodAppUI/utils/custom_functions.dart new file mode 100644 index 0000000..6e3b3b2 --- /dev/null +++ b/lib/FoodAppUI/utils/custom_functions.dart @@ -0,0 +1,6 @@ +import 'package:intl/intl.dart'; + +String formatCurrency(num amount,{int decimalCount = 0}){ + final formatCurrency = new NumberFormat.simpleCurrency(decimalDigits: decimalCount); + return formatCurrency.format(amount); +} \ No newline at end of file diff --git a/lib/FoodAppUI/utils/widget_functions.dart b/lib/FoodAppUI/utils/widget_functions.dart new file mode 100644 index 0000000..7067e96 --- /dev/null +++ b/lib/FoodAppUI/utils/widget_functions.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +Widget addVerticalSpace(double height){ + return SizedBox( + height:height + ); +} + +Widget addHorizontalSpace(double width){ + return SizedBox( + width:width + ); +} \ No newline at end of file diff --git a/lib/GoogleMaps/address_on_map_screen.dart b/lib/GoogleMaps/address_on_map_screen.dart new file mode 100644 index 0000000..a75ab3a --- /dev/null +++ b/lib/GoogleMaps/address_on_map_screen.dart @@ -0,0 +1,143 @@ +import 'dart:async'; + +import 'package:all_flutter_gives/GoogleMaps/location_service.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +class AddressOnMapScreen extends StatefulWidget { + AddressOnMapScreen({Key key}) : super(key: key); + + @override + _AddressOnMapScreenState createState() => _AddressOnMapScreenState(); +} + +class _AddressOnMapScreenState extends State { + TextEditingController _searchController = new TextEditingController(); + + // todo: Setting controller for the map + Completer _controller = Completer(); + + // todo: Setting two locations on the map + static final CameraPosition _home = CameraPosition( + target: LatLng(6.6890322, 3.2698028), + zoom: 15.1456, + ); + + static final CameraPosition _kLake = CameraPosition( + bearing: 192.8334901395799, + target: LatLng(6.6899919, 3.2733869), + tilt: 59.440717697143555, + zoom: 15); + + // todo: Adding Markers + static final Marker _kLakeMarker = Marker( + markerId: MarkerId('_kLake'), + infoWindow: InfoWindow(title: 'BreadSmith'), + icon: BitmapDescriptor.defaultMarker, + position: LatLng(6.6899919, 3.2733869), + ); + + static final Marker _homeMarker = Marker( + markerId: MarkerId('_home'), + infoWindow: InfoWindow(title: 'Home'), + icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueBlue), + position: LatLng(6.6890322, 3.2698028), + ); + + // todo: Adding Polylines + static final Polyline _kPolyline = Polyline( + polylineId: PolylineId('_kPolyline'), + points: [ + LatLng(6.6890322, 3.2698028), + LatLng(6.6899919, 3.2733869), + ], + width: 5); + + // todo: Adding Polygon : Drawing a line between 4 coordinates + static final Polygon _kPolygon = Polygon( + polygonId: PolygonId('_kPolygon'), + points: [ + LatLng(6.6890322, 3.2698028), + LatLng(6.6899919, 3.2733869), + LatLng(6.686, 3.285), + LatLng(6.670, 3.2753), + ], + strokeWidth: 5, + fillColor: Colors.transparent); + + @override + Widget build(BuildContext context) { + return new Scaffold( + appBar: AppBar( + title: Text('Google Maps'), + ), + body: Column( + children: [ + Row( + children: [ + Expanded( + child: TextFormField( + controller: _searchController, + textCapitalization: TextCapitalization.words, + decoration: InputDecoration(hintText: 'Search by City'), + onChanged: (val) { + print(val); + }, + ), + ), + IconButton( + onPressed: () async { + // todo: Get the address of a place in the search bar, use the 'getPlace' method to convert the address to coordinates, + // todo: then, call the '_goToPlace' method to move the map to the location + var place = await LocationService() + .getPlace(_searchController.text); + + _goToPlace(place); + }, + icon: Icon(Icons.search)) + ], + ), + Expanded( + child: GoogleMap( + mapType: MapType.normal, + trafficEnabled: true, + myLocationButtonEnabled: true, + myLocationEnabled: true, + polylines: {_kPolyline}, + polygons: {_kPolygon}, + initialCameraPosition: _home, + markers: {_kLakeMarker, _homeMarker}, + onMapCreated: (GoogleMapController controller) { + _controller.complete(controller); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: _goToTheLake, + label: Text('Go Home'), + icon: Icon(Icons.directions_boat), + ), + ); + } + + Future _goToPlace(Map place) async { + final double lat = place['geometry']['location']['lat']; + final double lng = place['geometry']['location']['lng']; + + // todo: Animate to _kLake location + final GoogleMapController controller = await _controller.future; + controller.animateCamera( + CameraUpdate.newCameraPosition( + CameraPosition(target: LatLng(lat, lng), zoom: 12), + ), + ); + } + + Future _goToTheLake() async { + // todo: Animate to _kLake location + final GoogleMapController controller = await _controller.future; + controller.animateCamera(CameraUpdate.newCameraPosition(_kLake)); + } +} diff --git a/lib/GoogleMaps/location_service.dart b/lib/GoogleMaps/location_service.dart new file mode 100644 index 0000000..dfaeb6c --- /dev/null +++ b/lib/GoogleMaps/location_service.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +class LocationService { + final String _key = + 'AIzaSyC9UyXEkIayXCAUkMOxA0nDKqyfCTyLKUo'; // 'AIzaSyAh42BiXzE4HbBTf5u4bwKFrBloFROk0WI'; + + Future getPlaceId(String input) async { + final String url = + 'https://maps.googleapis.com/maps/api/place/findplacefromtext/json?input=$input&inputtype=textquery&key=$_key'; + + var response = await http.get(Uri.parse(url)); + + var json = jsonDecode(response.body); + + print("calling getPlaces ${json}"); + + var placeId = json['candidates'][0]['place_id'] as String; + + print('getPlaceId $placeId'); + + return placeId; + } + + Future> getPlace(String input) async { + final placeId = await getPlaceId(input); + final String url = + 'https://maps.googleapis.com/maps/api/place/details/json?place_id=$placeId&key=$_key'; + + var response = await http.get(Uri.parse(url)); + + var json = jsonDecode(response.body); + + var results = json['result'] as Map; + + print('getPlace $results'); + + return results; + } +} diff --git a/lib/GoogleMaps/main.dart b/lib/GoogleMaps/main.dart new file mode 100644 index 0000000..9e0de3b --- /dev/null +++ b/lib/GoogleMaps/main.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +import 'address_on_map_screen.dart'; + +void main() => runApp(MyApp()); + +class MyApp extends StatelessWidget { + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: AddressOnMapScreen(), + ); + } +} diff --git a/lib/InfiniteListPagination/main.dart b/lib/InfiniteListPagination/main.dart new file mode 100644 index 0000000..659b412 --- /dev/null +++ b/lib/InfiniteListPagination/main.dart @@ -0,0 +1,135 @@ + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:pull_to_refresh/pull_to_refresh.dart'; + +import 'model/passenger_data.dart'; + + +// Based on this tutorial +// https://www.youtube.com/watch?v=KcRtURq-Ww8 + +void main() { + HttpOverrides.global = new MyHttpOverrides(); + + runApp(InfiniteListViewApp()); +} + +class MyHttpOverrides extends HttpOverrides { + @override + HttpClient createHttpClient(SecurityContext context) { + return super.createHttpClient(context) + ..badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + } +} + +class InfiniteListViewApp extends StatelessWidget { + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + debugShowCheckedModeBanner: false, + home: HomePage()); + } +} + +class HomePage extends StatefulWidget { + @override + _HomePageState createState() => _HomePageState(); +} + +class _HomePageState extends State { + int currentPage = 1; + + int totalPages; + + List passengers = []; + + final RefreshController refreshController = + RefreshController(initialRefresh: true); + + Future getPassengerData({bool isRefresh = false}) async { + if (isRefresh) { + currentPage = 1; + } else { + if (currentPage >= totalPages) { + refreshController.loadNoData(); + return false; + } + } + + final Uri uri = Uri.parse( + "https://api.instantwebtools.net/v1/passenger?page=$currentPage&size=10"); + + final response = await http.get(uri); + + if (response.statusCode == 200) { + final result = passengersDataFromJson(response.body); + + if (isRefresh) { + passengers = result.data; + }else{ + passengers.addAll(result.data); + } + + currentPage++; + + totalPages = result.totalPages; + + print(response.body); + setState(() {}); + return true; + } else { + return false; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Infinite List Pagination"), + ), + body: SmartRefresher( + controller: refreshController, + enablePullUp: true, + onRefresh: () async { + final result = await getPassengerData(isRefresh: true); + if (result) { + refreshController.refreshCompleted(); + } else { + refreshController.refreshFailed(); + } + }, + onLoading: () async { + final result = await getPassengerData(); + if (result) { + refreshController.loadComplete(); + } else { + refreshController.loadFailed(); + } + }, + child: ListView.separated( + itemBuilder: (context, index) { + final passenger = passengers[index]; + + return ListTile( + title: Text(passenger.name), + subtitle: Text(passenger.airline.country), + trailing: Text(passenger.airline.name, style: TextStyle(color: Colors.green),), + ); + }, + separatorBuilder: (context, index) => Divider(), + itemCount: passengers.length, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/InfiniteListPagination/model/passenger_data.dart b/lib/InfiniteListPagination/model/passenger_data.dart new file mode 100644 index 0000000..5585135 --- /dev/null +++ b/lib/InfiniteListPagination/model/passenger_data.dart @@ -0,0 +1,110 @@ + +// To parse this JSON data, do +// +// final passengersData = passengersDataFromJson(jsonString); + +import 'dart:convert'; + +PassengersData passengersDataFromJson(String str) => PassengersData.fromJson(json.decode(str)); + +String passengersDataToJson(PassengersData data) => json.encode(data.toJson()); + +class PassengersData { + PassengersData({ + this.totalPassengers, + this.totalPages, + this.data, + }); + + final int totalPassengers; + final int totalPages; + final List data; + + factory PassengersData.fromJson(Map json) => PassengersData( + totalPassengers: json["totalPassengers"], + totalPages: json["totalPages"], + data: List.from(json["data"].map((x) => Passenger.fromJson(x))), + ); + + Map toJson() => { + "totalPassengers": totalPassengers, + "totalPages": totalPages, + "data": List.from(data.map((x) => x.toJson())), + }; +} + +class Passenger { + Passenger({ + this.id, + this.name, + this.trips, + this.airline, + this.v, + }); + + String id; + String name; + int trips; + Airline airline; + int v; + + factory Passenger.fromJson(Map json) => Passenger( + id: json["_id"], + name: json["name"], + trips: json["trips"], + airline: Airline.fromJson(json["airline"]), + v: json["__v"], + ); + + Map toJson() => { + "_id": id, + "name": name, + "trips": trips, + "airline": airline.toJson(), + "__v": v, + }; +} + +class Airline { + Airline({ + this.id, + this.name, + this.country, + this.logo, + this.slogan, + this.headQuaters, + this.website, + this.established, + }); + + int id; + String name; + String country; + String logo; + String slogan; + String headQuaters; + String website; + String established; + + factory Airline.fromJson(Map json) => Airline( + id: json["id"], + name: json["name"], + country: json["country"], + logo: json["logo"], + slogan: json["slogan"], + headQuaters: json["head_quaters"], + website: json["website"], + established: json["established"], + ); + + Map toJson() => { + "id": id, + "name": name, + "country": country, + "logo": logo, + "slogan": slogan, + "head_quaters": headQuaters, + "website": website, + "established": established, + }; +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 2bc5163..6f391b1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,21 @@ +import 'dart:io'; + import 'package:all_flutter_gives/DesignSystemFlutter/flutter_design_screen.dart'; +import 'package:all_flutter_gives/FlutterWeb/routing/route_names.dart'; +import 'package:all_flutter_gives/FlutterWeb/routing/router.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'FlutterWeb/locator.dart'; +import 'FlutterWeb/services/navigation_service.dart'; import 'FlutterWeb/views/home/home_view.dart'; import 'FlutterWeb/views/layout_template/layout_template.dart'; +import 'FoodAppUI/screens/landing_screen.dart'; +import 'InfiniteListPagination/main.dart'; void main() { + HttpOverrides.global = new MyHttpOverrides(); + setupLocator(); runApp(MyApp()); } @@ -21,8 +30,11 @@ class MyApp extends StatelessWidget { primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: - FlutterWebLayoutTemplate() // FlutterDesignSample() // // SembastHomeScreen() + builder: (context, child) => FlutterWebLayoutTemplate(child: child), + navigatorKey: locator().navigatorKey, + onGenerateRoute: generateRoute, + initialRoute: HomeRoute, + // home: FlutterWebLayoutTemplate() // FlutterDesignSample() // // SembastHomeScreen() // BlocProvider( // create: (context) => AlbumsBloc(albumsRepo: AlbumServices()), // child: AlbumsScreen(), diff --git a/pubspec.lock b/pubspec.lock index 438a324..ca78516 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,13 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - animator: + _fe_analyzer_shared: dependency: transitive description: - name: animator + name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "36.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.1" archive: dependency: transitive description: @@ -21,21 +28,21 @@ packages: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.3.0" asn1lib: dependency: transitive description: name: asn1lib url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.1" auto_size_text_pk: dependency: transitive description: @@ -57,6 +64,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.1.4" characters: dependency: transitive description: @@ -70,7 +98,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -78,6 +106,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" collection: dependency: transitive description: @@ -85,6 +120,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + connectivity: + dependency: "direct main" + description: + name: connectivity + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.6" + connectivity_for_web: + dependency: transitive + description: + name: connectivity_for_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0+1" + connectivity_macos: + dependency: transitive + description: + name: connectivity_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1+2" + connectivity_platform_interface: + dependency: transitive + description: + name: connectivity_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" crypto: dependency: transitive description: @@ -99,13 +169,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.3" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.2" + dartz: + dependency: "direct main" + description: + name: dartz + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.1" encrypt: dependency: "direct main" description: name: encrypt url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "5.0.1" equatable: dependency: "direct main" description: @@ -126,14 +210,21 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.2" file: dependency: transitive description: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.1.0" + version: "6.1.2" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" flutter: dependency: "direct main" description: flutter @@ -146,6 +237,18 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.3" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" flutter_test: dependency: "direct dev" description: flutter @@ -156,6 +259,18 @@ packages: description: flutter source: sdk version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + url: "https://pub.dartlang.org" + source: hosted + version: "8.0.9" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" get: dependency: "direct main" description: @@ -170,22 +285,57 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "7.2.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + google_maps_flutter: + dependency: "direct main" + description: + name: google_maps_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.10" + google_maps_flutter_platform_interface: + dependency: transitive + description: + name: google_maps_flutter_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" http: dependency: "direct main" description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.2" + version: "0.13.4" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.4" + version: "4.0.0" + integration_test: + dependency: "direct dev" + description: + name: integration_test + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.1" intl: - dependency: transitive + dependency: "direct main" description: name: intl url: "https://pub.dartlang.org" @@ -204,14 +354,14 @@ packages: name: logging url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" lottie: dependency: "direct main" description: name: lottie url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.2.2" matcher: dependency: transitive description: @@ -225,7 +375,14 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" + mockito: + dependency: "direct dev" + description: + name: mockito + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.0" nested: dependency: transitive description: @@ -240,6 +397,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.0" + observable_ish: + dependency: transitive + description: + name: observable_ish + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" path: dependency: transitive description: @@ -253,42 +424,49 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.9" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.5" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.3" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.0" path_provider_windows: dependency: "direct main" description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.0" + version: "2.0.5" platform: dependency: transitive description: @@ -302,63 +480,100 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "1.0.3" pointycastle: dependency: transitive description: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.5.2" process: dependency: transitive description: name: process url: "https://pub.dartlang.org" source: hosted - version: "4.2.1" + version: "4.2.3" provider: - dependency: transitive + dependency: "direct main" description: name: provider url: "https://pub.dartlang.org" source: hosted version: "4.3.3" + provider_architecture: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "77165f34804cf3be22409647a50a73d0cfbc00d2" + url: "https://github.com/FilledStacks/provider_architecture" + source: git + version: "1.1.1+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + pull_to_refresh: + dependency: "direct main" + description: + name: pull_to_refresh + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" responsive_builder: dependency: "direct main" description: name: responsive_builder url: "https://pub.dartlang.org" source: hosted - version: "0.4.1" + version: "0.4.2" sembast: dependency: "direct main" description: name: sembast url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.2" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.13" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.10" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.3" shared_preferences_platform_interface: dependency: transitive description: @@ -372,19 +587,33 @@ packages: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.3" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + slide_to_act: + dependency: "direct main" + description: + name: slide_to_act + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" source_span: dependency: transitive description: @@ -399,20 +628,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.10.0" - states_rebuilder: + stream_channel: dependency: transitive description: - name: states_rebuilder + name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "4.0.0+1" - stream_channel: + version: "2.1.0" + stream_transform: dependency: transitive description: - name: stream_channel + name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.0.0" string_scanner: dependency: transitive description: @@ -420,6 +649,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + sync_http: + dependency: transitive + description: + name: sync_http + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" synchronized: dependency: transitive description: @@ -440,7 +676,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.2" typed_data: dependency: transitive description: @@ -461,28 +697,56 @@ packages: name: velocity_x url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "7.1.1" vxstate: dependency: transitive description: name: vxstate url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.1.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + webdriver: + dependency: transitive + description: + name: webdriver + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.3.6" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.2.0+1" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + dart: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" diff --git a/pubspec.yaml b/pubspec.yaml index d72a407..80585f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ environment: sdk: ">=2.7.0 <3.0.0" dependencies: - http: ^0.12.2 + http: ^0.13.4 path_provider: ^2.0.1 neumorphic: ^0.4.0 velocity_x: ^3.0.0 @@ -35,6 +35,20 @@ dependencies: path_provider_windows: ^2.0.1 shared_preferences: ^2.0.5 responsive_builder: ^0.4.1 + connectivity: ^3.0.6 + dartz: ^0.10.0 + provider_architecture: + git: https://github.com/FilledStacks/provider_architecture + provider: ^4.0.1 +# firebase_auth: 0.8.1 + pull_to_refresh: ^2.0.0 + slide_to_act: ^2.0.1 + intl: ^0.17.0 + google_fonts: ^2.1.0 + fluttertoast: ^8.0.7 + google_maps_flutter: ^2.0.6 +# geolocator: ^6.2.1 +# geocoding: ^1.0.5 flutter: sdk: flutter @@ -47,6 +61,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + mockito: ^5.0.17 + integration_test: any # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -62,6 +78,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/ + - assets/images/ # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see diff --git a/test/flutterMockTesting/cart_test.dart b/test/flutterMockTesting/cart_test.dart new file mode 100755 index 0000000..3ce090c --- /dev/null +++ b/test/flutterMockTesting/cart_test.dart @@ -0,0 +1,55 @@ +import 'package:all_flutter_gives/FlutterMockTesting/core/models/product.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/viewmodels/cart_model.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/helpers/dependency_assembly.dart'; +import 'package:flutter_test/flutter_test.dart'; + +List mockProducts = [ + Product(id: 1, name: "Product1", price: 111, imageUrl: "imageUrl"), + Product(id: 2, name: "Product2", price: 222, imageUrl: "imageUrl"), + Product(id: 3, name: "Product3", price: 333, imageUrl: "imageUrl"), + Product(id: 4, name: "Product4", price: 444, imageUrl: "imageUrl"), +]; + +void main() { + setupDependencyAssembler(); + + var cartViewModel = dependencyAssembler(); + + cartViewModel.addToCart(mockProducts[0]); + cartViewModel.addToCart(mockProducts[1]); + cartViewModel.addToCart(mockProducts[2]); + cartViewModel.addToCart(mockProducts[3]); + cartViewModel.addToCart(mockProducts[0]); + cartViewModel.addToCart(mockProducts[1]); + + group('Given Cart Page Loads', () { + test('Page should load list of products added to cart', () async { + expect(cartViewModel.cartSize, 6); + expect(cartViewModel.getCartSummary().keys.length, 4); + }); + + test( + 'Page should consolidate products in cart and show accurate summary data', + () { + cartViewModel.getCartSummary(); + expect(cartViewModel.getProduct(0).id, 1); + expect(cartViewModel.getProduct(1).id, 2); + expect(cartViewModel.getProduct(2).id, 3); + expect(cartViewModel.getProduct(3).id, 4); + + expect(cartViewModel.getProductQuantity(0), 2); + expect(cartViewModel.getProductQuantity(1), 2); + expect(cartViewModel.getProductQuantity(2), 1); + expect(cartViewModel.getProductQuantity(3), 1); + }); + + test('When user confirms the purchase, it should show total costs', () { + expect(cartViewModel.totalCost, 1443); + }); + + test('When user has finished the purchase, it should clear the cart', () { + cartViewModel.clearCart(); + expect(cartViewModel.cartSize, 0); + }); + }); +} diff --git a/test/flutterMockTesting/product_list_test.dart b/test/flutterMockTesting/product_list_test.dart new file mode 100755 index 0000000..6f35c68 --- /dev/null +++ b/test/flutterMockTesting/product_list_test.dart @@ -0,0 +1,51 @@ +import 'package:all_flutter_gives/FlutterMockTesting/core/models/product.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/services/api.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/viewmodels/cart_model.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/core/viewmodels/product_list_model.dart'; +import 'package:all_flutter_gives/FlutterMockTesting/helpers/dependency_assembly.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class MockAPI extends API { + @override + Future> getProducts() { + return Future.value([ + Product( + id: 1, + name: "MacBook Pro 16-inch model", + price: 2399, + imageUrl: "imageUrl"), + Product(id: 2, name: "AirPods Pro", price: 249, imageUrl: "imageUrl"), + ]); + } +} + +final Product mockProduct = + Product(id: 1, name: "Product1", price: 111, imageUrl: "imageUrl"); + +void main() { + setupDependencyAssembler(); + var productListViewModel = dependencyAssembler(); + productListViewModel.api = MockAPI(); + + var cartViewModel = dependencyAssembler(); + + group('Given Product List Page Loads', () { + test('Page should load a list of products from firebase', () async { + await productListViewModel.getProducts(); + + expect(productListViewModel.products.length, 2); + expect( + productListViewModel.products[0].name, 'MacBook Pro 16-inch model'); + expect(productListViewModel.products[0].price, 2399); + expect(productListViewModel.products[1].name, 'AirPods Pro'); + expect(productListViewModel.products[1].price, 249); + }); + }); + + test('when user adds a product to cart, badge counter should increment by 1', + () { + cartViewModel.addToCart(mockProduct); + + expect((cartViewModel.cartSize), 1); + }); +} diff --git a/test/flutterUnitTesting/email_password_validator_test.dart b/test/flutterUnitTesting/email_password_validator_test.dart new file mode 100644 index 0000000..7eb331e --- /dev/null +++ b/test/flutterUnitTesting/email_password_validator_test.dart @@ -0,0 +1,38 @@ +import 'package:all_flutter_gives/FlutterUnitTesting/login_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('title', () { + // Arrange (Initialization) + + // Act (Execution) + + // Assert (Observation) + }); + + group('Given the login page pops up', () { + test('empty email returns error string', () { + var result = EmailFieldValidator.validate(''); + + expect(result, 'Email can\'t be empty'); + }); + + test('non-empty email returns null', () { + var result = EmailFieldValidator.validate('email'); + + expect(result, null); + }); + + test('empty password returns error string', () { + var result = PasswordFieldValidator.validate(''); + + expect(result, 'Password can\'t be empty'); + }); + + test('non-empty password returns null', () { + var result = PasswordFieldValidator.validate('pass'); + + expect(result, null); + }); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart index cc2eda6..7da5580 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,11 +5,10 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. +import 'package:all_flutter_gives/main.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:all_flutter_gives/main.dart'; - void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ