From 140d54cb1c053931272d7ad4b18cec8151ffbcef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 8 Nov 2022 11:18:25 +0000 Subject: [PATCH 01/33] feat: Updating hows and whys. --- README.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7dadc87..3ddafa5 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,61 @@ -# learn-flutter -https://flutter.dev/ +
+ +![Learn](https://user-images.githubusercontent.com/17494745/200544789-0b024c77-0d49-4702-8866-b69f61521033.png) + +Learn the Flutter basics to get up-and-running **fast** and build **awesome cross-platform applications**! + +
+ ## What is Flutter? -Flutter in an open Source SDK for creating high-performance mobile apps for IOS and Android. -The Flutter makes it easier for you to build user interfaces, while reducing the amount of code required to create and update your app. +Flutter is an open-source framework created by Google +for creating multi-platform, high-performance applications +from a single codebase. It makes it easier for you to build +user interfaces that works both on web and mobile devices. + +Flutter uses [Dart](https://github.com/dwyl/learn-dart), a +general-purpose programming language created by Google. +If you come from an object-oriented language like `Java`, `C#`, +`Go` or `Javascript/Typescript`, you will feel right at home. ## Why use Flutter? -- Flutter can be used to build cross platform native applications (Android, IOS, Desktop and Web) using the same codebase. -- The Dart programming language used in Flutter is object oriented and familiar to most developers. -- Development times are significantly faster than other cross-platform frameworks thanks to stateful hot-reloading and excellent virtual device support. -- If we close the application when we open it again we can continue from where we were -- Flutter has a _complete_ design system with a library of Material UI widgets included which speed up the development process. +- Flutter can be used to build cross platform native applications +(Android, iOS, Desktop and Web) using the same codebase. +This significantly simplifies maintenance costs and dev headache +when deploying for either Android or iOS devices. + +- The Dart programming language used in Flutter + is object oriented and familiar to most developers. +Flutter benefits immensely by leveraging Dart. +Being a language optimized for UI and compiling to ARM +& x64 machine code for mobile, desktop and backend, +it offers amazing performance benchmarks. + +- Development times are significantly faster + than other cross-platform frameworks + thanks to stateful hot-reloading + and excellent virtual device support. +If we close the application, +when we open it again +we can continue from where we stopped. + +- Flutter has a _complete_ design system +with a library of Material UI widgets +included which speeds up the +development process. + +- It's growing at a fast-pace and being increasingly used +in production worldwide. + +https://user-images.githubusercontent.com/194400/84572723-e3b04800-ad93-11ea-85e2-19e9693e5a26.png + +- Flutter has overtaken React Native in searchs, +further showcasing the growing trend of Flutter. +Also, Flutter is [probably more performant](https://www.orientsoftware.com/blog/flutter-vs-react-native-performance/) +than React Native in mobile devices. + +https://user-images.githubusercontent.com/17494745/198244948-29e5d3a5-1b2b-4d1f-a434-d4eee2a5799c.png ## Core Principles From f0856ca03158d731326a167d8c4dfa86c9475b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 8 Nov 2022 11:51:06 +0000 Subject: [PATCH 02/33] feat: Adding installation steps. --- README.md | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3ddafa5..c67b9d8 100644 --- a/README.md +++ b/README.md @@ -48,14 +48,120 @@ development process. - It's growing at a fast-pace and being increasingly used in production worldwide. -https://user-images.githubusercontent.com/194400/84572723-e3b04800-ad93-11ea-85e2-19e9693e5a26.png +![fast-pace](https://user-images.githubusercontent.com/194400/84572723-e3b04800-ad93-11ea-85e2-19e9693e5a26.png) - Flutter has overtaken React Native in searchs, further showcasing the growing trend of Flutter. Also, Flutter is [probably more performant](https://www.orientsoftware.com/blog/flutter-vs-react-native-performance/) than React Native in mobile devices. -https://user-images.githubusercontent.com/17494745/198244948-29e5d3a5-1b2b-4d1f-a434-d4eee2a5799c.png +![rn](https://user-images.githubusercontent.com/17494745/198244948-29e5d3a5-1b2b-4d1f-a434-d4eee2a5799c.png) + +## Installing Flutter +Installing Flutter might seem like a daunting task. +But do not worry, we'll help you get your local environment +running in no time! Since we are targetting web and mobile, +there are a few tools and SDKs we ought to install first. + +These steps will be oriented to Mac/Unix devices but you should +be able to follow if you have a Windows device. If you're ever stuck, +don't be shy! Reach out to us and [open an issue](https://github.com/dwyl/learn-flutter/issues), +we'll get back to you as fast as we can! + +### Installing Flutter SDK +Head over to https://docs.flutter.dev/get-started/install, +select your operating system and follow the instructions. + +In our case, we're going to download the SDK for +our Mac. After downloading the SDK, you should extract +the `.zip` contents to a wanted location +(in our case, we extracted the folder to our `Home` - `cd ~`). + +Now, we ought to update our `PATH` variable so we can access +the binary we just downloaded to our command line. Open your terminal and: + +```sh +cd $HOME +nano .zshrc +``` + +And add `export PATH="$PATH:`pwd`/flutter/bin"` pointing +to the location where you extracted the folder. +Now, if you restart the terminal and type `flutter doctor`, +you should be able to run the command with no problems. + +`flutter doctor` checks your environment and displays a report to the +terminal window. It checks it all the necessary tools for development +for all devices are correctly installed. Let's do just that. + +> If you found this procedure convoluted, you can alternatively +install Flutter through [Homebrew](https://brew.sh/). +After installing Homebrew, you can install Flutter by simply running +`brew install --cask flutter`. + +### Install XCode +To install XCode, simply open your AppStore, search for 'XCode' +and press `Install`. It's that easy. + +image + +### Install Android Studio +Now targetting for Android devices, we need to install Android SDK and toolkits. +For this, we are going to install Android Studio and work from there. +Head over to https://developer.android.com/studio and download. + +After downloading, run the installer and select `Default settings` and let +the installer do its magic. After this, you should be prompted with the following window. + +image + +Click on the `More actions` dropdown and click on `SDK Manager`. +You should be prompted with this window. + +Screenshot 2022-11-08 at 11 41 29 + +After installing with default settings, you probably already have +an Android SDK installed. If that's the case, follow through +to `SDK Tools` and check on `Android SDK Command-line Tools`. + +image + +And then click `Finish`. This will install the command line tools. + +After installing, copy the `Android SDK Location` in the window. +Open a terminal window and type the following to add the SDK path +to the `Path` env variable. + +```sh +cd $HOME +nano .zshrc +``` + +and then add the SDK path you just copied, and save the file + +`export ANDROID_HOME=PATH_YOU_JUST_COPIED` + +Restart your terminal again and type `flutter doctor --android-licenses`. +This will prompt you to accept the Android licenses. Just type `y` as you read +through them to accept. + +### Installing Cocoapods +If you run `flutter doctor` again, you should see we are almost done. +You might see a text saying `CocoaPods not installed`. Let's fix that. + +Install [Homebrew](https://brew.sh/) and run `brew install cocoapods`. + +And you should be all sorted! + +### Adding plugins to Android Studio +If you happen to use Android Studio when developing, +adding the Flutter plugin will help you tremendously. +Just open Android Studio, click on `Plugins`, +search for "Flutter" and click `Install`. + +image + +You are asked to "Restart the IDE". Do so and ta-da :tada:, you are done! ## Core Principles From 49f137485f4f100c808ddfd0027d85306b47c3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 8 Nov 2022 11:53:45 +0000 Subject: [PATCH 03/33] feat: Adding last installation step. --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index c67b9d8..8773184 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,13 @@ search for "Flutter" and click `Install`. You are asked to "Restart the IDE". Do so and ta-da :tada:, you are done! +### Checking everything +If you run `flutter doctor`, you should have everything in the green. + +image + +Congratulations, give yourself a pat on the back, you are **all ready**! + ## Core Principles Flutter is designed for the creation of 2D mobile applications.
From 9a72b0885adeacc5f7308120950b535232767daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 8 Nov 2022 12:59:22 +0000 Subject: [PATCH 04/33] feat: Stateless widgets. --- README.md | 179 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 148 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 8773184..fec9e36 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Learn the Flutter basics to get up-and-running **fast** and build **awesome cros -## What is Flutter? +# What? πŸ’‘ Flutter is an open-source framework created by Google for creating multi-platform, high-performance applications @@ -18,7 +18,7 @@ general-purpose programming language created by Google. If you come from an object-oriented language like `Java`, `C#`, `Go` or `Javascript/Typescript`, you will feel right at home. -## Why use Flutter? +# Why? 🀷 - Flutter can be used to build cross platform native applications (Android, iOS, Desktop and Web) using the same codebase. @@ -57,7 +57,16 @@ than React Native in mobile devices. ![rn](https://user-images.githubusercontent.com/17494745/198244948-29e5d3a5-1b2b-4d1f-a434-d4eee2a5799c.png) -## Installing Flutter +# Who? πŸ‘€ +This repo is useful for anyone +that is interested in mobile and web app development. +For anyone that hasn't yet touched Flutter, this +repo is a *great* place to start to get your computer +ready for Flutter/Dart development, understand the +**main concepts** and *guide* you to then create +your very first Flutter app. + +# Installing Flutter ⬇️ Installing Flutter might seem like a daunting task. But do not worry, we'll help you get your local environment running in no time! Since we are targetting web and mobile, @@ -68,7 +77,7 @@ be able to follow if you have a Windows device. If you're ever stuck, don't be shy! Reach out to us and [open an issue](https://github.com/dwyl/learn-flutter/issues), we'll get back to you as fast as we can! -### Installing Flutter SDK +## Installing Flutter SDK Head over to https://docs.flutter.dev/get-started/install, select your operating system and follow the instructions. @@ -99,7 +108,7 @@ install Flutter through [Homebrew](https://brew.sh/). After installing Homebrew, you can install Flutter by simply running `brew install --cask flutter`. -### Install XCode +## Install XCode To install XCode, simply open your AppStore, search for 'XCode' and press `Install`. It's that easy. @@ -145,7 +154,7 @@ Restart your terminal again and type `flutter doctor --android-licenses`. This will prompt you to accept the Android licenses. Just type `y` as you read through them to accept. -### Installing Cocoapods +## Installing Cocoapods If you run `flutter doctor` again, you should see we are almost done. You might see a text saying `CocoaPods not installed`. Let's fix that. @@ -153,7 +162,7 @@ Install [Homebrew](https://brew.sh/) and run `brew install cocoapods`. And you should be all sorted! -### Adding plugins to Android Studio +## Adding plugins to Android Studio If you happen to use Android Studio when developing, adding the Flutter plugin will help you tremendously. Just open Android Studio, click on `Plugins`, @@ -163,47 +172,155 @@ search for "Flutter" and click `Install`. You are asked to "Restart the IDE". Do so and ta-da :tada:, you are done! -### Checking everything +## Checking everything If you run `flutter doctor`, you should have everything in the green. image Congratulations, give yourself a pat on the back, you are **all ready**! -## Core Principles +# Core Principles 🐣 -Flutter is designed for the creation of 2D mobile applications.
+If you have had experience in mobile development prior to Flutter, +either be it React Native or native, you will find the learning curve +quite manageable, as Flutter's foundation is built upon a few principles +that are present in both. Let's take a look at these :smile:. ## Widgets -In Flutter _everything_ is a "Widget". -A Widget is a UI building block you can use to assemble your app. -In the following Gif the sample application contains a total of 6 widgets: -![flutter-counter-sample](https://user-images.githubusercontent.com/194400/74101695-87fb5700-4b34-11ea-9fbd-09cc6bf3ed41.gif) +In Flutter _everything_ is a **Widget**. + +A Widget is a UI building block you can use to assemble your app. +If you come from `React`, you may find these akin to `components`. + +You will build your UI out of widgets. They essentially describe +what *their view should look like* given their current state. +If their state changes, the widget rebuilds and checks the diff +to determine the minimal changes to transition from the state `t0` to `t1`. + + +In the following `.gif` the sample application contains a total of 6 widgets: + +![widget-gif](https://user-images.githubusercontent.com/194400/74101695-87fb5700-4b34-11ea-9fbd-09cc6bf3ed41.gif) + Image attribution: https://uxplanet.org/why-you-should-use-google-flutter-42f2c6ba036c -1. The **container** widget starting on line 17 groups all other widgets in the layout. + +1. The **container** widget `Scaffold` starting on line 38 groups all other widgets in the layout. 2. The ***`appBar`*** widget displays the text "Flutter Demo Home Page" 3. The ***`body`*** contains a **child** widget which in turn has **Text** and a **$_counter** placeholder. -4. The **`floatingActionButton`** is the button that gets clicked, it contains a **child** which is the icon. -Examples of Widgets include dialog windows, buttons, icons, menus, scroll bars and cards. -You can use one of the many built-in Material UI widgets or create your own from scratch.
+4. The ***`floatingActionButton`*** is the button that gets clicked, +it contains a **child** which is the icon. +Examples of Widgets include +dialog windows, buttons, icons, menus, scroll bars and cards. +You can use one of the many built-in Material UI widgets +or create your own from scratch. A widget can be defined as: - Physical elements of an application (buttons, menus or bars) - Visual elements such as colors -+ Layout and positioning of elements on the screen using a grid system - -Widgets are assembled in declarative hierarchy which allows us to easily organise the layout of our App as a series of nested widgets.
- -![Screen Shot 2020-02-06 at 19 01 17](https://user-images.githubusercontent.com/27420533/73969408-4475d280-4913-11ea-8384-99c863321155.png) +- Layout and positioning of elements on the screen using a grid system + +Widgets are assembled in declarative hierarchy +which allows us to easily organize +the layout of our App as a series of nested widgets.
+ +![system](https://user-images.githubusercontent.com/27420533/73969408-4475d280-4913-11ea-8384-99c863321155.png) + +Screens are composed of several small widgets that have only one job. +Groups of widgets are assembled together to build a functional application. + +For example, a Container widget contains other widgets + that have functions like layout, placement and size. + +A basic screen layout is controlled by combining +a container and other smaller widgets as their children. +This was seen in the gif above. The `Scaffhold` widget +warps three widgets. + +Remember, Widgets aren't necessarily visual elements within the application. +In the gif above, the second child `body` widget also uses +a widget named `Center` that, as the name implies, centers +its children within the screen. It's *controlling* the +aspects of their child and displaying them centered. +There are several other widgets that have a similar behaviour, +such as padding, alignment, row, columns, and grids. + +### Stateless widgets +Widgets are not all stateless. Stateless widgets never change. +They receive arguments from their parent, store them in `final` member variables +(`final` is analogous to a `const`ant variable). When a widget is asked +to `build()`. it uses these stored values and renders. +Here's what a stateless widget looks like: + +```dart +class MyAppBar extends StatelessWidget { + const MyAppBar({required this.title, super.key}); + + // Fields in a Widget subclass are always marked "final". + + final Widget title; + + @override + Widget build(BuildContext context) { + return Container( + height: 56.0, // in logical pixels + padding: const EdgeInsets.symmetric(horizontal: 8.0), + decoration: BoxDecoration(color: Colors.blue[500]), + // Row is a horizontal, linear layout. + child: Row( + children: [ + const IconButton( + icon: Icon(Icons.menu), + tooltip: 'Navigation menu', + onPressed: null, // null disables the button + ), + // Expanded expands its child + // to fill the available space. + Expanded( + child: title, + ), + const IconButton( + icon: Icon(Icons.search), + tooltip: 'Search', + onPressed: null, + ), + ], + ), + ); + } +} +``` -Screens are composed of several small widgets that have only one job. Groups of widgets are assembled together to build a functional application.
-For example a Container widget contains other widgets that have functions like layout, placement and size.
+We notice straight away the widget is a subclass of +[`StatelessWidget`](https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html). + +All widgets have a `Key key` (`super.key`) as optional parameter in their ocnstructor. +It is by the flutter engine at the step of recognizingg which widget +in a list as changed. It's more useful when you have a list of widgets +*of the same type* that can potentially be removed or inserted. + +This `MyAppBar` widget takes as argument a `title`. +This effectively becomes the `field` of the widget, +and is used in the `Expanded` children widget. +Additionally, since this is a widget (more specifically, +a subclass of `Stateless Widget`), we have to +implement the `build()` function. This is what is rendered. + +This widget could be used in a container and be one of its childrens +like so: + +```dart + MyAppBar( + title: Text( + 'Example title', + ), + ), +``` -Screen layout is controlled by combining a container and other smaller widgets.
+Simple enough, right? -There are some widgets that have no physical form within the application instead their goal and control some aspects of another Widget.
-Like: padding, alignment, row, columns, and grids.
+### Stateful widgets +While stateless widgets are static (never change) ## Layers @@ -305,7 +422,7 @@ As you can see if you go to the windows prompt and run the command "flutter" it - ## Scaffold class? +## Scaffold class? Provides a framework which implements the basic material design visual layout structure of the Flutter app. Contais various functionality from giving an appbar, a floating button, a drawer, background color, bottom navigation bar and body. @@ -320,12 +437,12 @@ As you can see if you go to the windows prompt and run the command "flutter" it Any widget in the body is positioned at the top left corner by default. - ## FloatingActionButton + #### FloatingActionButton Is a button displayed floating in the bottom right corner. We use this button to promote a primary action in the application. - ## Drawer + #### Drawer Is a panel displayed to the side of the body. One usually has to swipe left to right of right to left to access the drawer. From 534308c5fb1aa78539d2b02d1862b0ad78747d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 8 Nov 2022 13:26:26 +0000 Subject: [PATCH 05/33] feat: Stateful widgets. --- README.md | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fec9e36..0d90b9c 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,9 @@ either be it React Native or native, you will find the learning curve quite manageable, as Flutter's foundation is built upon a few principles that are present in both. Let's take a look at these :smile:. +If you want an in-depth guide and learn every aspect of Flutter, +check the official documentation -> https://flutter.dev/learn + ## Widgets In Flutter _everything_ is a **Widget**. @@ -320,7 +323,98 @@ like so: Simple enough, right? ### Stateful widgets -While stateless widgets are static (never change) +While stateless widgets are static (never change), +**stateful widgets** are dynamic. For example, +they change its appearance or behaviour according +to events triggered by user interaction or when +it receives data. + +For example `Checkbox`, `Slider`, `Textfield` are examples +of stateful widgets - subclass of +[`StatefulWidget`](https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html). +A widget's state is stored in a `State` object. +Therefore, we *separate* the widget's state from its appearance. +Whenever the state changes, the `State` object calls `setState()`, +thus rerendering the widget. + +Let's see some code! + +```dart +import 'package:flutter/material.dart'; + +class Counter extends StatefulWidget { + // This is the state object, different from the appearance. + // It holds the state configuration and + // the values provided by the parent and used by the build method + // of the State (no values are provided in this instance) + // Fields in a Widget subclass are always marked + + const Counter({super.key}); + + @override + State createState() => _CounterState(); +} + +class _CounterState extends State { + int _counter = 0; + + void _increment() { + setState(() { + // This call to setState tells the Flutter framework + // that something has changed in this State, which + // causes it to rerun the build method below so that + // the display can reflect the updated values. If you + // change _counter without calling setState(), then + // the build method won't be called again, and so + // nothing would appear to happen. + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + // This method is rerun every time setState is called, + // for instance, as done by the _increment method above. + // The Flutter framework has been optimized to make + // rerunning build methods fast, so that you can just + // rebuild anything that needs updating rather than + // having to individually changes instances of widgets. + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: _increment, + child: const Text('Increment'), + ), + const SizedBox(width: 16), + Text('Count: $_counter'), + ], + ); + } +} + +void main() { + runApp( + const MaterialApp( + home: Scaffold( + body: Center( + child: Counter(), + ), + ), + ), + ); +} +``` + +Let's unpack the code above. The `StatefulWidget` and `State` are separate objects. +The former (being the first one) declares its state by using the State object. +The State object is declared right after, initializing a `_counter` at 0. +It declares an `_increment()` function that calls `setState()` +(indicating the state is going to be changed) and increments the `_counter` variable. + +As with any widget, the `build()` method makes use of the `_counter` variable +to display the number of times the button is pressed. Everytime it is pressed, +the `_increment()` function is called, effectively changing the state and incrementing it. ## Layers From 7505c1b809d35e53b47f71aab8923de0c97584e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 8 Nov 2022 13:55:42 +0000 Subject: [PATCH 06/33] feat: Layouts. --- README.md | 84 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 0d90b9c..75ada2a 100644 --- a/README.md +++ b/README.md @@ -416,39 +416,81 @@ As with any widget, the `build()` method makes use of the `_counter` variable to display the number of times the button is pressed. Everytime it is pressed, the `_increment()` function is called, effectively changing the state and incrementing it. -## Layers +## Layout -A Flutter layout can have layers to create a visual effect where certain widgets "float" on top of others to give them priority. +As we've already stated, the core of Flutter are widgets. +In fact, almost everything is a widget - even layout models. +The things you see are widgets. +![image](https://user-images.githubusercontent.com/17494745/200579851-de25d19d-5c80-4033-8491-c2ff452f7137.png) -![Screen Shot 2020-02-07 at 09 06 06](https://user-images.githubusercontent.com/27420533/74015797-36629900-4989-11ea-8ec1-757aecad18ce.png) +But things that you *don't see* are also widgets. +We mentioned this before but we'll understand it better now. +For any web or mobile app development, +we need to create layouts to organize our components in and +make it look *shiny* :shiny: and *good-looking* :art:. +This example is taken from the official docs +-> https://docs.flutter.dev/development/ui/layout#lay-out-a-widget -Flutter uses layers to represent visual hierarchy -and relative importance or priority of each widget.
+Layout | Layout with padding and delimited borders +:-------------------------:|:-------------------------: +![](https://docs.flutter.dev/assets/images/docs/ui/layout/lakes-icons.png) | ![](https://docs.flutter.dev/assets/images/docs/ui/layout/lakes-icons-visual.png) -## Building Widgets -To assemble our widgets into an application we use the `build()` function. e.g: -`Widget build(BuildContext context) { ...` +So, you may ask, **how many widgets are there in this menu**? +Great question! There are visible widgets but also widgets that +*help us* lay out the items correctly, center them and space +them evenly to make it look good. +Here's how the widget tree looks like for this menu. -For example, the `appBar` menu -needs to be invoked using the **`build()`** function. -It can then have other nested "child" widgets -such as buttons or text.
+![widget_tree](https://docs.flutter.dev/assets/images/docs/ui/layout/sample-flutter-layout.png) -## User Interaction +The nodes in *pink* are containters. They are *not visible* +but help us customize its child widget by adding +`padding`, `margin`, `border`, `background color`, etc... + +Let's see a code example of an invisible widget that will +center a text in the middle of the screen. + +```dart +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration(color: Colors.white), + child: const Center( + child: Text( + 'Hello World', + textDirection: TextDirection.ltr, + style: TextStyle( + fontSize: 32, + color: Colors.black87, + ), + ), + ), + ); + } +} +``` + +The `Center` widget centers all its children inside of it. +`Center` is *invisible* but is a widget nonetheless. +This yields the following result. + +![invisible_result](https://docs.flutter.dev/assets/images/docs/ui/layout/hello-world.png) + + +See? Isn't it so simple? :tada: + +As you would do in React, you can whatever Layout you wish just +by encapsulating widgets (akin to components) and ordering +them accordingly. -A **`StatefulWidget`** widget, as its' name suggests, stores the **`state`** of the UI it represents. -For example the **`counter`** widget stores the **_counter** variable -which keeps track of the number of times the user has clicked the button. -All user interaction that requires storing some data/input uses a **`StatefulWidget`** widget.
-When the State of an object is changed, the `setState()` function -should be called to update the UI. -This in turn will invoke the `build` method -which re-renders the widget. ## Testing From de0f0b3f204fc0211a6b635f9b8625fe37ff7a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 8 Nov 2022 15:26:57 +0000 Subject: [PATCH 07/33] feat: Adding assets. --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/README.md b/README.md index 75ada2a..9dbd6f2 100644 --- a/README.md +++ b/README.md @@ -489,8 +489,66 @@ As you would do in React, you can whatever Layout you wish just by encapsulating widgets (akin to components) and ordering them accordingly. +## Assets +For any application, sometimes we need images and assets to display to the user. +Common resources are image files, static data (`JSON` files), +videos, icons... + +In Flutter, we use the `pubspec.yaml` file +(often located at the root of the project) to +require assets in the app. + +```yaml +flutter: + assets: + - directory/ + - assets/my_icon.png +``` + +> There's a nuanced behavior when loading assets. +> If you have two files ` .../graphics/background.png` and +> `.../graphics/dark/background.png` and the `pubspec.yaml` file +> contains the following: + +> ```yaml +> flutter: +> assets: +> - graphics/background.png +> ``` + +> Both are imported and included in the asset bundle. +> One is considered the **main asset** and the other +> a **variant**. +> This behaviour is useful for images on different resolutions. + +There are two ways of accessing the loaded access. +Each Flutter app has a `RootBundle` for easy access +to the main asset bundle. You can import directly +using the `rootBundle` global static. +However, inside a widget context, it's recommended to obtain the +asset bundle for the widget `BuildContext` using the +[`DefaultAssetBundle`](https://api.flutter.dev/flutter/widgets/DefaultAssetBundle-class.html). +This approach allows the parent widget to substitute a different +asset bundle at runtime, which is useful for localization +or testing purposes. + +Here's a code example for the `rootBundle` approach. +```dart +import 'package:flutter/services.dart' show rootBundle; +Future loadAsset() async { + return await rootBundle.loadString('assets/config.json'); +} +``` + +Here's a code example for the recommended approach inside +a widget. + +```dart +String data = await DefaultAssetBundle.of(context).loadString("assets/data.json"); +final jsonResult = jsonDecode(data); //latest Dart +``` ## Testing From 1698cce42afa051f7aeb39e553544bff50129d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 8 Nov 2022 16:01:29 +0000 Subject: [PATCH 08/33] feat: Adding navigation. --- README.md | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/README.md b/README.md index 9dbd6f2..3353571 100644 --- a/README.md +++ b/README.md @@ -550,6 +550,102 @@ String data = await DefaultAssetBundle.of(context).loadString("assets/data.json" final jsonResult = jsonDecode(data); //latest Dart ``` +## Navigation and routing +Most web and mobile apps aren't just a single page. +The user needs to navigate between screens to do whatever +action needs to be done, be it checking the details of a +product or just wanting to see the shopping cart. + +Flutter provides a `Navigator` widget to display screns as a stack, +using the native transition animations of the target device. +Navigating between screens necessitates the route's +`BuildContext` (which can be accessed through the widget) and +is made by calling methods like `push()` and `pop()`. + +Here's a code showcasing navigating between two routes. + +```dart +import 'package:flutter/material.dart'; + +void main() { + runApp(const MaterialApp( + title: 'Navigation Basics', + home: FirstRoute(), + )); +} + +class FirstRoute extends StatelessWidget { + const FirstRoute({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('First Route'), + ), + body: Center( + child: ElevatedButton( + child: const Text('Open route'), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SecondRoute()), + ); + }, + ), + ), + ); + } +} + +class SecondRoute extends StatelessWidget { + const SecondRoute({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Second Route'), + ), + body: Center( + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Go back!'), + ), + ), + ); + } +} +``` + +This basic code example shocases two routes, +each one containing only a single button. +Tapping the one on the first route +will navigate to the second route. +Clicking on the button of the second route +will return the user to the first route. +We are using the `Navigator.push()` and `Navigator.pop()` +functions to achieve this, by passing the context of +the widget. +Additionally, we are leveraging `MaterialPageRoute` to +transition between routes using a platform-specific animation +according to the [Material Design guidelines](https://m3.material.io/). + +Here's how it should look! + +![navigating_gif](https://user-images.githubusercontent.com/17494745/200613079-f65baeee-a822-4a58-b075-ce169d751325.gif) + + +If your application necessitates advanced navigation and routing requirements +(which is often the case with web apps that use direct links to each screen, +or an app with multiple `Navigator` widgets), you should consider using a +routing package like [`go_router`](https://pub.dev/packages/go_router). +This package allows one to parse the route path and configure the `Navigator` +whenever an app receives, for example, a deep link. + + ## Testing As in all programming languages, frameworks or platforms the secret to a successful Flutter application is to test it _extensively_. @@ -1021,3 +1117,9 @@ Then I saved the file and now going to the terminal Flutter is already recognize - Including Google codelabs in the learning process - Adding the Flutter GitHub repository in the learning process - Opting for Google’s free beginner Flutter learning course. Google provides a free course for learners. + + + + + +## Fazer sample app simples \ No newline at end of file From 3c45e813d1544bf5f056ee2311b7254b3b818e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 8 Nov 2022 17:03:21 +0000 Subject: [PATCH 09/33] feat: Add networking. --- README.md | 144 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/README.md b/README.md index 3353571..06da150 100644 --- a/README.md +++ b/README.md @@ -646,6 +646,150 @@ This package allows one to parse the route path and configure the `Navigator` whenever an app receives, for example, a deep link. +## Networking +For most apps, fetching data from the internet is a must. +Luckily, fetching data from the internet is a breeze. Let's do it! + +Firstly, we need to add the [`http`](https://pub.dev/packages/http) +package to the dependencies section in the `pubspec.yaml` file. +This file can be found at the route of your project. + +Let's add the package to the dependency list and import it. + +```yaml +dependencies: + http: 0.13.5 +``` + +```dart +import 'package:http/http.dart' as http; +``` + +We also need to change the `AndroidManifest.xml` file to +add Internet permission on Android devices. This file can be found in the +`android/app/src/main` on newly created projects. Add the following line. + +```xml + + +``` + + +Now, to make a network request is as easy +as apple pie. Check the following code. + +```dart +Future fetchAlbum() { + return http.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/1')); +} +``` + +By calling `http.get()`, it returns a [`Future`](https://github.com/dwyl/learn-dart#asynchronous-events) +that contains a `Response`. `Future` is a class to work with async operations. +It represents a potential value that will occur in the future. + +While `http.Response` has our data, it's much more useful to translate it +to a logical class. We can convert `http.Response` to a `Todo` class, +representing a "todo item". Let's create that class! + +```dart +class Todo { + final int id; + final String title; + final bool completed + + const Todo({ + required this.id, + required this.title, + required this.completed + }); + + factory Todo.fromJson(Map json) { + return Todo( + id: json['id'], + title: json['title'], + completed: json['completed'], + ); + } +} +``` + +We can create a function that makes the http request and, +if it is successful, tries to parse the data and create a +`Todo` object or raise an an error if the http request is +unsuccessful. + +```dart +Future fetchTodos() async { + final response = await http + .get(Uri.parse('https://jsonplaceholder.typicode.com/todos/1')); + + if (response.statusCode == 200) { + // If the server did return a 200 OK response, + // then parse the JSON. + return Todo.fromJson(jsonDecode(response.body)); + } else { + // If the server did not return a 200 OK response, + // then throw an exception. + throw Exception('Failed to load todos'); + } +} +``` + +How would we call this inside a widget? +We could do this inside `initState()`! +It is called exactly one time and never again! +Do **not** put an API call in the `build()` method +(unless you know what you are doing). +This method is called every time a render occurs, +which is quite often! + +```dart +class _MyAppState extends State { + late Future futureTodo; + + @override + void initState() { + super.initState(); + futureTodo = fetchTodo(); + } + // Β·Β·Β· +} +``` + +Finally, to display the data, we would want to use the +`FutureBuilder` widget. As the name implies, it's a +widget made to handle async data operations. + +```dart +FutureBuilder( + future: futureTodo, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text(snapshot.data!.title); + } else if (snapshot.hasError) { + return Text('${snapshot.error}'); + } + + // By default, show a loading spinner. + return const CircularProgressIndicator(); + }, +) +``` + +The `future` paramter relates to object we want to work with. +In this case, it is a parsed `Todo` object. + +The `builder` function tells Flutter what needs to be rendered, +depending on the current state of `Future`, which can +be *loading*, *success* or *error*. +Depending on the result of the operation, we +either show the error, the data or a loading animation +while we wait for the http request to fulfill. + +Isn't it easy? =) + + ## Testing As in all programming languages, frameworks or platforms the secret to a successful Flutter application is to test it _extensively_. From 560fbb157256e6e1eb3c09e043815df2bd7a2205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 8 Nov 2022 17:18:47 +0000 Subject: [PATCH 10/33] fix: Update windows install guide. --- README.md | 137 +++++++++++++++++++++++++----------------------------- 1 file changed, 64 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 06da150..5f9be42 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,64 @@ If you run `flutter doctor`, you should have everything in the green. Congratulations, give yourself a pat on the back, you are **all ready**! + +### Installing for Windows devices + +Installing Flutter on Windows should be the same process +as the one on MacOS'. However, setting up the `$PATH` +variable is different, since some terminal commands +can't be used in Windows since it's not an Unix-based +operating system. + +With this in mind, here's a quick rundown of how things +should go if you're installing Flutter on Windows. +Firstly, head over to https://docs.flutter.dev/get-started/install/windows +and download the `.zip` file. + +![download](https://i.imgur.com/ZNPFKsl.png) + +Extract the file and place the folder in directory `C:`. +It's probably best to create a folder in the directory like this. + + +![cdrive](https://i.imgur.com/76IAhtp.png) + +This is the console that comes with the Flutter folder +you just downloaded. You can see the devices connected or +even create a project through here. + +![flutterconsole](https://i.imgur.com/rJe2Uao.png) + +In order to access Flutter commands through the terminal, +instead of having to open this console, we need to update +our environment variables. + +You need to go to the bin folder of the extracted +`.zip` you downloaded and pasted on the `C:` drive +and copy the path. +Then, go to the computer properties, then go to advanced system settings. + +![properties](https://i.imgur.com/tKZP0ZG.png) + +Click on environment variables, +go to edit path and paste the path to the extracted +Flutter folder. + +![properties2](https://i.imgur.com/OoUtlWO.png) +![properties3](https://i.imgur.com/1IvNuGT.png) + + +As you can see, if you open a new Windows terminal +(also known as `windows prompt`) and +run the `flutter` command, this should prop up. + + ![run_command](https://i.imgur.com/oSCrjRM.png) + +The rest of the steps should be straight forward. +Just follow the ones on the Mac device. +Installing Android Studio is the exact same procedure +:smile:. + # Core Principles 🐣 If you have had experience in mobile development prior to Flutter, @@ -790,6 +848,12 @@ while we wait for the http request to fulfill. Isn't it easy? =) + + + + + + ## Testing As in all programming languages, frameworks or platforms the secret to a successful Flutter application is to test it _extensively_. @@ -808,81 +872,8 @@ Integration tests serve to test the application as a whole so that we can test t Before you dive into Flutter you have to learn the programming language that is used to build Flutter apps, and that is Dart. -Flutter for Windows installation -========== -Access the link and click Get Started - -![Flutter](https://i.imgur.com/35POvJA.png) - -Select the windows button and then click the "flutter_windows.zip" button. -![Flutter](https://i.imgur.com/ZNPFKsl.png) - -Extract the file and place the folder in directory "C:"
-It's probably best to create a folder in the directory like this. - -![Flutter](https://i.imgur.com/76IAhtp.png) - -This is the console that comes inside the folder in this case the Flutter console, can use to see which devices connected, create a project in Flutter. - -![Flutter](https://i.imgur.com/rJe2Uao.png) - -In order to access Flutter commands without having to open this console, we can use the Windows prompt itself we need to add Flutter to the environment variables.
- -You need to go to the bin folder and copy the path then go to the computer properties, then go to advanced system settings. - -![Flutter](https://i.imgur.com/tKZP0ZG.png) - -Click on environment variables, then go to edit path and paste the path to Flutter. - -![Flutter](https://i.imgur.com/OoUtlWO.png) ![Flutter](https://i.imgur.com/1IvNuGT.png) - - -As you can see if you go to the windows prompt and run the command "flutter" it already appears. - - ![Flutter](https://i.imgur.com/oSCrjRM.png) - - - Now at the windows prompt if you run the command "flutter doctor" will check if there is anything left to install so that we can develop the applications with Flutter. - - ![Flutter](https://i.imgur.com/6SMzguK.png) - - If you need to install android studio here is the link:https://developer.android.com/studio
- To install dart and flutter just go to the Android studio and press plugins and search for both.
- After having everything installed just open the Android Studio that will appear "Start a new Flutter Project". - - ![Flutter](https://i.imgur.com/1zxPJSP.png) - - - -## Scaffold class? - - Provides a framework which implements the basic material design visual layout structure of the Flutter app. - Contais various functionality from giving an appbar, a floating button, a drawer, background color, bottom navigation bar and body. - - - ### AppBar - It defines what has to be displayed at the top of the screen. - Has various properties like title,padding,brightness. - - ### Body - It's the area below the Appbar and behind the buttons. - Any widget in the body is positioned at the top left corner by default. - - - #### FloatingActionButton - - Is a button displayed floating in the bottom right corner. - We use this button to promote a primary action in the application. - - #### Drawer - - Is a panel displayed to the side of the body. - One usually has to swipe left to right of right to left to access the drawer. - It uses the Drawer properties which is a material design panel that slides from the edge of a Scaffold to show links in an application. - - Signing in with Google using Flutter ========== From 31310d25736ab2ffcc22833c36971b26920fc6b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 8 Nov 2022 18:36:03 +0000 Subject: [PATCH 11/33] fix: Fixing persistent data. --- README.md | 441 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 247 insertions(+), 194 deletions(-) diff --git a/README.md b/README.md index 5f9be42..962c103 100644 --- a/README.md +++ b/README.md @@ -846,6 +846,253 @@ either show the error, the data or a loading animation while we wait for the http request to fulfill. Isn't it easy? =) + +## Local databases +Sometimes, when writing an app, we need to persist +and query large amounts of data on the local device. +In these cases, it is beneficial considering +using a database instead of a local file or a key-value store. + +In this walkthrough, we are going to present +two alternatives: SQLite and ObjectBox. + +### SQLite + +SQLite is one of the most popular methods for storing data locally. +For this demo, we will use the package +[`sqflite`](https://pub.dev/packages/sqflite). + +Sqflite is one of the most used and updated packages +to connect to SQLite databases in Flutter. + +#### 1. Add the dependencies +To work with SQLite databases, we need +to import two dependencies. +We'll use `sqflite` to interact with the SQLite database, +and `path` to define the location for storing the database +on disk. + + +```dart +dependencies: + flutter: + sdk: flutter + sqflite: + path: +``` + +And import the packages in the file you are working in. + +```dart +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; +``` + +#### 2. Define a model +Let's take a look at the data we are going to store. +Let's define a class for the table we are going to create +in SQLite. + +```dart +class Item { + final int id; + final String text; + final bool completed; + + const Item({ + required this.id, + required this.text, + required this.completed, + }); + + // Convert an Item into a Map. The keys must correspond to the names of the + // columns in the database. + Map toMap() { + return { + 'id': id, + 'text': text, + 'completed': completed, + }; + } + + // Implement toString to make it easier to see information about + // each item when using the print statement. + @override + String toString() { + return 'Item{id: $id, text: $text, completed: $completed}'; + } +} +``` + +#### 3. Open connection to the database +To open a connection to the SQLite database, +we are going to define the path to the database file +using `path` +**and** +open the database with `sqflite`. + +```dart + +WidgetsFlutterBinding.ensureInitialized(); + +// Open the database and store the reference. +final database = openDatabase( + // Set the path to the database. Note: Using the `join` function from the + // `path` package is best practice to ensure the path is correctly + // constructed for each platform. + join(await getDatabasesPath(), 'item_database.db'), +); +``` + +#### 4. Creating table +To create the table to store our items, we must first +verify the number of columns and type refer +exactly to the ones we defined in the class. +After this, it's just a matter of running the appropriate +`SQL` expression to create the table. + +```dart +final database = openDatabase( + + join(await getDatabasesPath(), 'item_database.db'), + + // When the database is first created, create a table to store items. + onCreate: (db, version) { + // Run the CREATE TABLE statement on the database. + return db.execute( + 'CREATE TABLE items(id INTEGER PRIMARY KEY, text TEXT, completed INTEGER)', + ); + }, + // Set the version. This executes the onCreate function and provides a + // path to perform database upgrades and downgrades. + version: 1, +); +``` + +#### 5. CRUD operations + +Now that we have a database created, alongside the +table, to create, update, list and insert Items is +quite easy! Check the following piece of code. + +```dart +Future crudOperations(Item item) async { + // Get a reference to the database + final db = await database; + + // Insert an Item into the table. + await db.insert('items', item.toMap()) + + // Retrieve list of items + // and convert the List into a List. + final List> maps = await db.query('items'); + Item[] items = List.generate(maps.length, (i) { + return Item( + id: maps[i]['id'], + name: maps[i]['text'], + age: maps[i]['completed'], + ); + }); + + // Update the given Item. + await db.update( + 'items', + item.toMap(), + // Ensure that the Item has a matching id. + where: 'id = ?', + // Pass the Item's id as a whereArg to prevent SQL injection. + whereArgs: [item.id], + ); + + // Remove the Item from the database. + await db.delete( + 'items', + // Use a `where` clause to delete a specific item. + where: 'id = ?', + // Pass the Item's id as a whereArg to prevent SQL injection. + whereArgs: [id], + ); +} +``` + +And there you have it! Here is a quick rundown of the +process of creating a database, a table and +applying CRUD operations on it. You can leverage +this database to hold large amounts of data locally +(up to a limit, of course) instead of relying +on common files. + + +### ObjectBox +There are alternatives to SQLite, such as Hive and `ObjectBox`. +In this section, we are going to just reference +`ObjectBox` so the user knows there isn't one single +database option. + +`ObjectBox` provides a NoSQL database that uses a +pure Dart API, so there is no need to learn +and write SQL expressions. There are performance +advantages to using this library. Make sure +to read the [package docs](https://github.com/objectbox/objectbox-dart#flutter-database-for-fast-dart-object-persistence-) +to find out if this option is best for you. + +Here is how basic setup and CRUD +operations would work using `ObjectBox`. + +```dart +// Annotate a Dart class to create a box +@Entity() +class Person { + @Id() + int id; + String name; + + Person({this.id = 0, required this.name}); +} + +// Put a new object into the box +var person = Person(name: "Joe Green"); +final id = box.put(person); + +// Get the object back from the box +person = box.get(id)!; + +// Update the object +person.name = "Joe Black"; +box.put(person); + +// Query for objects +final query = (box.query(Person_.name.equal("Joe Black")) + ..order(Person_.name)).build(); +final people = query.find(); +query.close(); + +// Remove the object from the box +box.remove(person.id); +``` + + + + + + + + + + + + + + + + + + + + @@ -993,209 +1240,15 @@ On your Manifest.xml add below the package name this line: Then just edit the button as you like and the Google login is fully functional. -How to use SQLite in Flutter -========== - - -![Flutter](https://i.imgur.com/27rAotE.png) - -Persisted data ( persitent Date ) are very important for users, since they would be inconvenient to always be writing your information or wait for the network carry the same data again. In these situations, it would be best to store your data locally. - -# Why SQLite? - -SQLite is one of the most popular methods for storing data locally. For this article, we will use the package ( package ) sqflite acceded to SQLite. Sqflite is one of the most used and updated packages to connect to SQLite databases in Flutter. - - -# How to use Sqflite on Flutter? - - -## 1. Add the dependency to the project
-In our project, we will open the file pubspec.yaml and search for dependencies. Under dependencies we add the latest version of sqflite and path_provider(which can be removed from pub.dev). - - -```ruby - dependencies: - flutter: - sdk: flutter - sqflite: any - path_provider: any -``` - - -## 2. Creating a DB Client
-Now, in our project, we will create a new Database.dart file.
- -1- Creation of a private builder that can be used only within the class: - - -```ruby -class DBProvider { - DBProvider._(); - static final DBProvider db = DBProvider._(); -} -``` - -2- Preparation of the database -Next we will create the database object and provide you with a getter, which will create an instance of the database, if it has not already been created. - - -```ruby -static Database _database; - - Future get database async { - if (_database != null) - return _database; - - // if _database is null we instantiate it - _database = await initDB(); - return _database; - } -``` - -If no objects are assigned to the database, we use the initDB function to create the database. In this function, we get the directory where we will store the database and create the tables we want: - - -```ruby -initDB() async { - Directory documentsDirectory = await getApplicationDocumentsDirectory(); - String path = join(documentsDirectory.path, "TestDB.db"); - return await openDatabase(path, version: 1, onOpen: (db) { - }, onCreate: (Database db, int version) async { - await db.execute("CREATE TABLE Client (" - "id INTEGER PRIMARY KEY," - "first_name TEXT," - "last_name TEXT," - "blocked BIT" - ")"); - }); - } -``` -## 3. Creation of Model Class
- -The data inside our database will be converted to Dart Maps, so first we need to create the Model Classes with the 'toMap' and 'fromMap' methods. - - -```ruby - -/// ClientModel.dart -import 'dart:convert'; -Client clientFromJson(String str) { - final jsonData = json.decode(str); - return Client.fromMap(jsonData); -} - -String clientToJson(Client data) { - final dyn = data.toMap(); - return json.encode(dyn); -} - -class Client { - int id; - String firstName; - String lastName; - bool blocked; - - Client({ - this.id, - this.firstName, - this.lastName, - this.blocked, - }); - - factory Client.fromMap(Map json) => new Client( - id: json["id"], - firstName: json["first_name"], - lastName: json["last_name"], - blocked: json["blocked"] == 1, - ); - - Map toMap() => { - "id": id, - "first_name": firstName, - "last_name": lastName, - "blocked": blocked, - }; -} -``` - - - -## 4. CRUD Operations
- -Using 'Insert': - - -```ruby - -newClient(Client newClient) async { - final db = await database; - var res = await db.insert("Client", newClient.toMap()); - return res; - } -``` - -Get Client via an ID: - - -```ruby -getClient(int id) async { - final db = await database; - var res =await db.query("Client", where: "id = ?", whereArgs: [id]); - return res.isNotEmpty ? Client.fromMap(res.first) : Null ; - } -``` - -Obtain all Clients with one condition: -In this example, we use rawQuery to map the results list to a list of Client objects: - -```ruby - -getAllClients() async { - final db = await database; - var res = await db.query("Client"); - List list = - res.isNotEmpty ? res.map((c) => Client.fromMap(c)).toList() : []; - return list; - } -``` -Update an existing Client: -```ruby -updateClient(Client newClient) async { - final db = await database; - var res = await db.update("Client", newClient.toMap(), - where: "id = ?", whereArgs: [newClient.id]); - return res; - } -``` -Delete a Client: -```ruby -deleteClient(int id) async { - final db = await database; - db.delete("Client", where: "id = ?", whereArgs: [id]); - } -``` - -Delete All Clients: - -```ruby - - -deleteAll() async { - final db = await database; - db.rawDelete("Delete * from Client"); - } - -``` - ## Trouble-Shooting Section From a2e52a0511f7f91367506ebe2961718fe0526ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 8 Nov 2022 19:30:13 +0000 Subject: [PATCH 12/33] feat: Add state management. --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 962c103..f4ac73b 100644 --- a/README.md +++ b/README.md @@ -1074,12 +1074,53 @@ query.close(); box.remove(person.id); ``` - - - - - - +## State management +We have previously mentioned state within a widget. +In stateful widgets, the state and how/when it changes +determines how many times the widget is rendered. +State that can be neatly contained in a single widget +is referred as "local state" or **ephemeral state**. +Other parts of the widget tree seldom need to access this kind of state. + +However, there is state that is *not ephemeral* +and usually is needed across many widgets of the app. +This shared state is usually called **application state**. +Examples of these are user preferences or a shopping cart +in an e-commerce app. + +Consider the following gif, taken directly +from the `Flutter` docs +-> https://docs.flutter.dev/development/data-and-backend/state-mgmt/intro + +![cart](https://docs.flutter.dev/assets/images/docs/development/data-and-backend/state-mgmt/state-management-explainer.gif) + + +Each widget in the widget tree might have its own +local state but there's a piece of *application state* +(i.e. shared state) in the form of a cart. +This cart is accessible from any widget of the app - +in this case, the `MyCart` widget uses it to list what +item was added to it. + +There are [many approaches to state management](https://docs.flutter.dev/development/data-and-backend/state-mgmt/options), +so it's up to you to decide which options are best +suited for your use case. Many people recommend +[`Provider`](https://pub.dev/packages/provider) or +[`Riverpod`](https://riverpod.dev/). + +[Bloc](https://bloclibrary.dev/#/) is also an increasingly +popular alternative which forces the logic and the UI +to be implemented separately. + +State management and which alternative is best +is a [big point of contention](https://www.reddit.com/r/FlutterDev/comments/w4osgi/for_you_what_is_the_best_state_management_with/) +between developers. There is no bad option, just choose whichever +you think it's best. + +We shall not delve too much into state management as +shared app state is not a beginner-friendly topic +to learn and is often very opinionated. As long +as you understood *what it is*, it's awesome! :tada: From 4e7ec23bc6a97006fa8c1fefe56485c1bce441e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Wed, 9 Nov 2022 09:35:16 +0000 Subject: [PATCH 13/33] feat: Adding testing #68 --- README.md | 405 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 394 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f4ac73b..b6cd58d 100644 --- a/README.md +++ b/README.md @@ -1123,41 +1123,424 @@ to learn and is often very opinionated. As long as you understood *what it is*, it's awesome! :tada: +# Testing πŸ§ͺ +As in all programming languages, frameworks or platforms, +the secret to a successful application is to test it _extensively_. +Implementing tests is not only advantageous to catch bugs +but also avoid regression when implementing new features. +> To learn more about an example of using TDD: +> https://github.com/dwyl/flutter-counter-example +> If you are interested in a more thorough introduction +> to testing and debugging in various IDEs with Flutter, please +> take a look at the [official docs](https://docs.flutter.dev/testing/debugging) +## Unit testing +Unit testing are handy to verify the behaviour +of a single function/method/class. +Let's add some unit tests in Flutter, shall we? +Firstly, we ought to import the [`test`](https://pub.dev/packages/test) +which offers the core functionality for writing tests in Dart. +```dart +dev_dependencies: + test: 1.22.0 +``` + +And now, let's create a simple class and a referring test +file to test it. Create two files so you have the following +folder structure. + +``` +counter_app/ + lib/ + counter.dart + test/ + counter_test.dart +``` + +In `counter.dart`, add the following piece of code. + +```dart +class Counter { + int value = 0; + + void increment() => value++; + + void decrement() => value--; +} +``` + +In `counter_test.dart`, add the following: + +```dart +// Import the test package and Counter class +import 'package:counter_app/counter.dart'; +import 'package:test/test.dart'; + +void main() { + group('Counter', () { + test('value should start at 0', () { + expect(Counter().value, 0); + }); + + test('value should be incremented', () { + final counter = Counter(); + + counter.increment(); + + expect(counter.value, 1); + }); + + test('value should be decremented', () { + final counter = Counter(); + + counter.decrement(); + + expect(counter.value, -1); + }); + }); +} +``` + +We can group tests using the `group()` function. In each +`test()` we use the `expect()` function to compare +expected assertions. + +You can type the following command to run the tests +we just created: + +```sh +flutter test test/counter_test.dart +``` + +### Mock testing +Sometimes functions fetch data from web services or databases. +When we are unit testing these, it is inconvenient to do so +because calling external dependencies may slow down +the execution time. Needless to say, this external dependency +may sometimes be down, amongst other scenarios. + +In these situations, it is useful to **mock** +these dependencies. In Flutter, the *de facto* way of +mocking classes and objects is using the +[`mockito`](https://pub.dev/packages/mockito) +package. + +In this small section, we are going to add +this dependency, create a function to test +and mock a test file with a mock `http.Client`. + +Firstly, add the `mockito` package to the `pubspec.yaml` +file, along with the `flutter_test` dependency +(will provide core testing functionalities) and the +`http` package for HTTP requests. +Do take note that each test dependency will be +added to the `dev_dependencies` section of the file. + +```dart +dependencies: + http: 0.13.5 +dev_dependencies: + flutter_test: + sdk: flutter + mockito: 5.3.2 + build_runner: 2.3.2 +``` +Now let's create a function to test. +This function will fetch data from the internet. +```dart +Future fetchAlbum(http.Client client) async { + final response = await client + .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')); + if (response.statusCode == 200) { + // If the server did return a 200 OK response, + // then parse the JSON. + return Album.fromJson(jsonDecode(response.body)); + } else { + // If the server did not return a 200 OK response, + // then throw an exception. + throw Exception('Failed to load album'); + } +} +``` +You might have noticed the `http.Client` is provided +to the argument. This makes it so that the client +that fetches data changes according to any situation. +In Flutter, we can provide an `http.IOClient`. +For testing, we can pass a mock `http.Client`. +In a test file, we will add an an annotation to the main function +to generate a `MockClient` class with `mockito`. +According to the argument passed to the annotation, +the generated `MockClient` class will implement it. +When generating, the mocks will be located in a file +named `XX_test.mocks.dart`. We will import this file to use them. +For now, create a test file where we will add tests. +```dart +import 'package:http/http.dart' as http; +import 'package:mocking/main.dart'; +import 'package:mockito/annotations.dart'; +// Generate a MockClient using the Mockito package. +// Create new instances of this class in each test. +@GenerateMocks([http.Client]) +void main() { +} +``` + +Now run `flutter pub run build_runner build`. +This command will generate the mocks in `XX_test.mocks.dart`. +Now we can use these mocks in our tests! +Let's add two: one for a successful request and +another for a failing one, and catch the raised exception. + +```dart +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:mocking/main.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'fetch_album_test.mocks.dart'; + +// Generate a MockClient using the Mockito package. +// Create new instances of this class in each test. +@GenerateMocks([http.Client]) +void main() { + group('fetchAlbum', () { + test('returns an Album if the http call completes successfully', () async { + final client = MockClient(); + + // Use Mockito to return a successful response when it calls the + // provided http.Client. + when(client + .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'))) + .thenAnswer((_) async => + http.Response('{"userId": 1, "id": 2, "title": "mock"}', 200)); + + expect(await fetchAlbum(client), isA()); + }); + test('throws an exception if the http call completes with an error', () { + final client = MockClient(); + // Use Mockito to return an unsuccessful response when it calls the + // provided http.Client. + when(client + .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'))) + .thenAnswer((_) async => http.Response('Not Found', 404)); + expect(fetchAlbum(client), throwsException); + }); + }); +} +``` +In these tests, we are **importing** the generated mocks +(`fetch_album_test.mocks.dart`). +Plus, we create the `MockClient()`, define the behaviour +we expect the mock to do, and then pass it to the function, +effectively asserting its output. +We can run the tests and see if they fail or not +by running -## Testing +```sh +flutter test test/fetch_album_test.dart +``` + +Congratulations! You just mocked a `http.Client` object +and properly tested a function that used an external dependency. +`mockito` has many other features. +You can read about them +[in their documentation](https://pub.dev/packages/mockito). -As in all programming languages, frameworks or platforms the secret to a successful Flutter application is to test it _extensively_. -Several tests should created included functional tests and UX tests -which can help us discover and fix any bugs before users see them!
+## Integration testing +While unit testing is useful for testing individual +classes, functions or widgets, they don't +test how all of these *work together*, as a whole. +These tasks are captured and tested +with **integration tests**. -An application must have tested all functions, classes or tasks needed to run correctly without errors.
+We can luckily leverage the SDK's +[`integration_test`](https://github.com/flutter/flutter/tree/main/packages/integration_test) +package to do this. -Widget tests are necessary to confirm that they are performing their intended function -and correctly positioned in the layout.
-Integration tests serve to test the application as a whole so that we can test the app in a real-world scenario. +Let's start by creating a super simple app. +This app will just have a button and a counter +displaying the number of time the button was clicked. -> To learn more about an example of using TDD: https://github.com/dwyl/flutter-counter-example +But, before that, let's add the needed dependencies. +We'll be adding the `integration_test` and `flutter_test` +packages to the `dev_dependencies` section of `pubspec.yaml`. + +```yaml +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter +``` + +Now, let's create our app. In `lib/app.dart`, +let's use the following piece of code. + +```dart +import 'package:flutter/material.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Counter App', + home: MyHomePage(title: 'Counter App Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'You have pushed the button this many times:', + ), + Text( + '$_counter', + // Provide a Key to this specific Text widget. This allows + // identifying the widget from inside the test suite, + // and reading the text. + key: const Key('counter'), + style: Theme.of(context).textTheme.headline4, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + // Provide a Key to this button. This allows finding this + // specific button inside the test suite, and tapping it. + key: const Key('increment'), + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ); + } +} +``` + +Now, inside `integration_test/app_test.dart`, we are +going to test the action of clicking and checking +if the counter is incremented. For this, we will +initialize a singleton service `IntegrationTestWidgetsFlutterBinding`, +which executes the tests on a physical device and +leverage the `WidgetTester` class to interact +with the widgets. + +```dart +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'package:counter_app/main.dart' as app; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('end-to-end test', () { + testWidgets('tap on the floating action button, verify counter', + (tester) async { + app.main(); + await tester.pumpAndSettle(); + + // Verify the counter starts at 0. + expect(find.text('0'), findsOneWidget); + + // Finds the floating action button to tap on. + final Finder fab = find.byTooltip('Increment'); + + // Emulate a tap on the floating action button. + await tester.tap(fab); + + // Trigger a frame. + await tester.pumpAndSettle(); + + // Verify the counter increments by 1. + expect(find.text('1'), findsOneWidget); + }); + }); +} +``` + +Running these on mobile devices is the same +process as before - just run `flutter test integration_test`. +However, if you were to run these on a web browser, +you'd need to download [`ChromeDriver`](https://chromedriver.chromium.org/downloads), +create a file in `test_driver/integration_test.dart` with + +```dart +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); +``` + +and open two terminal windows. +In the first one, we launch `chromedriver` with + +```sh +chromedriver --port=4444 +``` + +and in the other, from the root of the project, run flutter +with the drive file path we just created and +targetting the test file we want to test, like so: + +```dart +flutter drive \ + --driver=test_driver/integration_test.dart \ + --target=integration_test/app_test.dart \ + -d chrome +``` -## Dart Before Flutter? -Before you dive into Flutter you have to learn the programming language that is used to build Flutter apps, and that is Dart. +And you're done! Congratulations, you just +unit *and* integration tested your application. +Awesome work! :tada: From eed4438f4df2cfa36fd5660f8f87dbb0c9e1e39d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Wed, 9 Nov 2022 09:54:28 +0000 Subject: [PATCH 14/33] feat: Refactoring login database tutorial to its own file. --- README.md | 191 ---------------------------------------- login-firebase-tutorial | 125 ++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 191 deletions(-) create mode 100644 login-firebase-tutorial diff --git a/README.md b/README.md index b6cd58d..9e87fd5 100644 --- a/README.md +++ b/README.md @@ -1543,195 +1543,4 @@ unit *and* integration tested your application. Awesome work! :tada: - - -Signing in with Google using Flutter -========== - -After a lot of research I realized that the only way to log in to Google without using Firebase is through the packages provided by Google.
- Using these packages we can make the user log in without saving their personal data, except the data we want to show , like email and username. - - ![Flutter](https://i.imgur.com/A4kNbpQ.png) - -Let's get started. First of all we have to create a new Flutter Project.
-Choose the Flutter Application option.
- - ![Flutter](https://i.imgur.com/b6fVARr.png) - -Insert the Name of the Project and the company name.
-After that in the 'main.dart' remove all the code except the main and switch the (MyApp) to (MaterialApp).
-Insert 'Title'.
- - ![Flutter](https://i.imgur.com/dLfAgUO.png) - - Create a new class to extend a 'StatefulWidget'.
- Change return 'Container' to 'Scaffold' so we can use the features provided by the Scaffold Class.
- - ![Flutter](https://i.imgur.com/Dfe11We.png) - - Enter in 'pubspec.yaml' , then go to google and search for google sign in package flutter or click this link: - https://pub.dev/packages/google_sign_in, go to installing and add that command to 'pubspec dependecies'. - - - ![Flutter](https://i.imgur.com/TQ18gDR.png) - - ![Flutter](https://i.imgur.com/iUtoy36.png) - - Click on "Packages get" as we made a change to the packages to update. - - - ![Flutter](https://i.imgur.com/81h384m.png) - - Import the Google package to 'main.dart'. - - ![Flutter](https://i.imgur.com/AKibdLx.png) - - Create the 'GoogleSignIn' object, provide the scope profile and email to that object. - - ![Flutter](https://i.imgur.com/9E5uVXH.png) - - You need to create a SignInAccount object.
- Name that object 'user' to be used to see if he is signed in or signed out.
- - Inside the 'Scaffold' use the 'AppBar' to add a Title.
- - ![Flutter](https://i.imgur.com/mbgBIlY.png) - - In the 'body' create a method '_buildbody'.
- Inside that method you need to add a 'ListTile' to display the information,then use a GoogleUserCircleAvatar to display the profile image.
- - ![Flutter](https://i.imgur.com/4HObCG5.png) - - Make the title and subtitle show the 'Name' and 'Email' on the screen. - Create a button to be pressed with the text 'Sign out'. - -![Flutter](https://i.imgur.com/yUXiw3F.png) - -![Flutter](https://i.imgur.com/bLaPMsK.png) - - - - In short, if the user is not connected, a message appears saying "Not Signed in". If this user is already connected, his data is displayed and a button appears saying "Sign Out". - - -Now, create both _GetSignIn and _GetSignOut methods.
-Use the method silently/ SignInSilently(); -> Used to Sign In the user without interaction. - -![Flutter](https://i.imgur.com/LK7YprE.png) - -After the UI is completed we need to register the App in the Firebase.
-Even if we don't need to use Firebase the App has to be registered there.
- -So go to: https://console.firebase.google.com/u/0/ -In the Firebase console, add a new project and get the same package name from your app. - - -![Flutter](https://i.imgur.com/AGFilMM.png) - -You will need to get the SHA-1 so go to 'gradlew' and to the terminal and run the command- 'gradlew signinReport'. - -![Flutter](https://i.imgur.com/9JD4DGP.png) - -![Flutter](https://i.imgur.com/YnweBh6.png) - -Insert the SHA-1 in the Firebase space then, download the file and paste it inside the Android > app.
- -Follow the steps to add the Firebase SDK / Go to people API:
- -https://developers.google.com/people/v1/getting-started - -Follow those 3 steps. - -![Flutter](https://i.imgur.com/ro6RiCJ.png) - -After that go to credentials and click the user data option.
-Click on the API and go for Android or IOS.
-Name your app again, put the SHA-1 you got from 'gradlew' and the package name you can find in your 'AndroidManifest.xml'.
- - -![Flutter](https://i.imgur.com/wkLI4L0.png) - - -Set up the consent screen, go to support email and select your email then click save.
-Go to OAuth 2.0 Cliente IDs, select the Android option.
- -On your Manifest.xml add below the package name this line: - - -```ruby - -``` - -Then just edit the button as you like and the Google login is fully functional. - - - - - - - - - - - -## Trouble-Shooting Section - -The *first* time I tried to change the path of the Flutter folder so that it could be accessed by the terminal without having to use several paths, it **did not work**, every time I closed the terminal Flutter was no longer recognized.
- -`command not found: flutter` - -Whenever I used *this* command:
- - -`export PATH="$PATH:~/development/flutter/bin` - -Then I started to notice that this command only worked for the current terminal, so it was not **permanent**. -Even using the command that should change the path permanently Flutter was still **unrecognized**.
- -This would be the command that would make Flutter access through the terminal permanent:
- -`export PATH="$PATH:[PATH_TO_FLUTTER_GIT_DIRECTORY]/flutter/bin"` - -Changing the path to where we would have cloned Flutter's Git repository [PATH_TO_FLUTTER_GIT_DIRECTORY]. -And then using the command: - -`source ~/.bash_profile` - - -What will help us refresh the terminal.
-But even so, every time I closed and opened the terminal again Flutter was no longer recognized.
-So I thought that maybe the best option would be to go to the document and edit it with the "export PATH" command but inside the document itself other than in the terminal.
- -So I went to Finder / Macintosh HD / Users / and to my user. -How is a hidden file or document we must type **`cmd` + `shift` + `.`** - -![finder-view-showing-flutter-directory](https://user-images.githubusercontent.com/27420533/74145756-83e43d80-4bf7-11ea-94ca-f6be3cfac21b.png) - -Then I opened my **`.zhs_profile`** file and typed the command: - -`export PATH=/Users/m/Documents/flutter/bin:$PATH` - -This instructs my terminal environment to export an updated version of the `PATH` environment variable with `flutter/bin` path _prepended_ to the current `PATH` variable. (_i.e. add `flutter/bin` to the `PATH` so that my terminal knows where to find the `flutter` CLI_) - -Then I saved the file and now going to the terminal Flutter is already recognized and can be accessed without having to run any commands. - - -![flutter-command-output-truncated](https://user-images.githubusercontent.com/27420533/74146245-9a3ec900-4bf8-11ea-8dca-63b549552ff4.png) - - - - -## How can you improve your learning in Flutter? - -- Get to know the Flutter platform vividly -- Thoroughly acquaint with all the features of Flutter -- Read through Flutter’s official documentation as it include easy examples that beginners would find helpful. The documents are seasoned with iOS and Android devices. The developers can easily interact with the present device -- Including Google codelabs in the learning process -- Adding the Flutter GitHub repository in the learning process -- Opting for Google’s free beginner Flutter learning course. Google provides a free course for learners. - - - - - ## Fazer sample app simples \ No newline at end of file diff --git a/login-firebase-tutorial b/login-firebase-tutorial new file mode 100644 index 0000000..4ef9076 --- /dev/null +++ b/login-firebase-tutorial @@ -0,0 +1,125 @@ + +# Signing in with Google using Flutter + +After a lot of research I realized that the only way to log in to Google without using Firebase is through the packages provided by Google.
+ Using these packages we can make the user log in without saving their personal data, except the data we want to show , like email and username. + + ![Flutter](https://i.imgur.com/A4kNbpQ.png) + +Let's get started. First of all we have to create a new Flutter Project.
+Choose the Flutter Application option.
+ + ![Flutter](https://i.imgur.com/b6fVARr.png) + +Insert the Name of the Project and the company name.
+After that in the 'main.dart' remove all the code except the main and switch the (MyApp) to (MaterialApp).
+Insert 'Title'.
+ + ![Flutter](https://i.imgur.com/dLfAgUO.png) + + Create a new class to extend a 'StatefulWidget'.
+ Change return 'Container' to 'Scaffold' so we can use the features provided by the Scaffold Class.
+ + ![Flutter](https://i.imgur.com/Dfe11We.png) + + Enter in 'pubspec.yaml' , then go to google and search for google sign in package flutter or click this link: + https://pub.dev/packages/google_sign_in, go to installing and add that command to 'pubspec dependecies'. + + + ![Flutter](https://i.imgur.com/TQ18gDR.png) + + ![Flutter](https://i.imgur.com/iUtoy36.png) + + Click on "Packages get" as we made a change to the packages to update. + + + ![Flutter](https://i.imgur.com/81h384m.png) + + Import the Google package to 'main.dart'. + + ![Flutter](https://i.imgur.com/AKibdLx.png) + + Create the 'GoogleSignIn' object, provide the scope profile and email to that object. + + ![Flutter](https://i.imgur.com/9E5uVXH.png) + + You need to create a SignInAccount object.
+ Name that object 'user' to be used to see if he is signed in or signed out.
+ + Inside the 'Scaffold' use the 'AppBar' to add a Title.
+ + ![Flutter](https://i.imgur.com/mbgBIlY.png) + + In the 'body' create a method '_buildbody'.
+ Inside that method you need to add a 'ListTile' to display the information,then use a GoogleUserCircleAvatar to display the profile image.
+ + ![Flutter](https://i.imgur.com/4HObCG5.png) + + Make the title and subtitle show the 'Name' and 'Email' on the screen. + Create a button to be pressed with the text 'Sign out'. + +![Flutter](https://i.imgur.com/yUXiw3F.png) + +![Flutter](https://i.imgur.com/bLaPMsK.png) + + + + In short, if the user is not connected, a message appears saying "Not Signed in". If this user is already connected, his data is displayed and a button appears saying "Sign Out". + + +Now, create both _GetSignIn and _GetSignOut methods.
+Use the method silently/ SignInSilently(); -> Used to Sign In the user without interaction. + +![Flutter](https://i.imgur.com/LK7YprE.png) + +After the UI is completed we need to register the App in the Firebase.
+Even if we don't need to use Firebase the App has to be registered there.
+ +So go to: https://console.firebase.google.com/u/0/ +In the Firebase console, add a new project and get the same package name from your app. + + +![Flutter](https://i.imgur.com/AGFilMM.png) + +You will need to get the SHA-1 so go to 'gradlew' and to the terminal and run the command- 'gradlew signinReport'. + +![Flutter](https://i.imgur.com/9JD4DGP.png) + +![Flutter](https://i.imgur.com/YnweBh6.png) + +Insert the SHA-1 in the Firebase space then, download the file and paste it inside the Android > app.
+ +Follow the steps to add the Firebase SDK / Go to people API:
+ +https://developers.google.com/people/v1/getting-started + +Follow those 3 steps. + +![Flutter](https://i.imgur.com/ro6RiCJ.png) + +After that go to credentials and click the user data option.
+Click on the API and go for Android or IOS.
+Name your app again, put the SHA-1 you got from 'gradlew' and the package name you can find in your 'AndroidManifest.xml'.
+ + +![Flutter](https://i.imgur.com/wkLI4L0.png) + + +Set up the consent screen, go to support email and select your email then click save.
+Go to OAuth 2.0 Cliente IDs, select the Android option.
+ +On your Manifest.xml add below the package name this line: + + +```ruby + +``` + +Then just edit the button as you like and the Google login is fully functional. + + + + + + + From b10ccbc3aabf8719674bbac108caf097d796c970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Wed, 9 Nov 2022 10:29:16 +0000 Subject: [PATCH 15/33] fix: Removing unneeded guides. --- rest-api-tutorial.md | 107 ------------------------------------------ tdd-example/README.md | 1 - 2 files changed, 108 deletions(-) delete mode 100644 rest-api-tutorial.md delete mode 100644 tdd-example/README.md diff --git a/rest-api-tutorial.md b/rest-api-tutorial.md deleted file mode 100644 index 806edde..0000000 --- a/rest-api-tutorial.md +++ /dev/null @@ -1,107 +0,0 @@ -# REST API - -Is An Application program Interface (API) that uses HTTP requests to GET, PUT, POST and DELETE data.
-is based on representational state transfer (REST) technology, an architectural style and approach to communications often used in web services development.
-An API for a website is code that allows two software programs to communicate with each another . The API spells out the proper way for a developer to write a program requesting services from an operating system or other application.

- -> ***A REST API defines a set of functions which developers can perform requests and receive responses via HTTP protocol.***. - -![Rest-Api](https://i.imgur.com/ZwQ2L6k.png) - - -## Why? - -All the processing is done on the server side. So the server has to do more work.
-The data is not separated from the page.
-In REST API, you ask the API server what you need and it sends you just the information you ask for, no additional formatting is done in the server.
-There is no need for unnecessary processing in the server. So, the performance of your website and apps are naturally improved. Also, you can use the same data in your website, desktop app, Android and iOS apps. - - -## How? -### Create a New Project - -Create a new flutter project in Android Studio and name it as you like. - -![Rest-Api](https://i.imgur.com/tRsrVWO.png) - -### Making an API Request In Flutter - -In this API request we make an API call to:https://jsonplaceholder.typicode.com/posts. -If you dont know what 'jsonplaceholder' is its a Fake Online Rest API for Testing and Prototyping. - -First include the http package in 'pubspec.yaml' file.
-Add this line under dependencies.
- -```ruby -dependencies: - flutter: - sdk: flutter - http: ^0.12.0 -``` -Import the http package in your 'main.dart' file: - -```ruby -import 'package:http/http.dart' as http; -``` - -Create a function getData() which will fetch the data from the API. - -```ruby -Future getData(){ - - } -``` -We’ll be making an API call which can take some time to return a response. This situation calls for async.
-Basically, we’ll need to wait till the api call completes and returns a result. As soon as it does, we’ll display the list.
-We’ll make the api call using http object and wait for it to complete.
- -```ruby -Future getData() async { - var response = await http.get( - Uri.encodeFull("https://jsonplaceholder.typicode.com/users/1/albums"), - headers: {"Accept": "application/json"}); - - setState(() { - data = json.decode(response.body); - }); - return "Success"; - } -``` - -To decode the data you need to use: - -```ruby -import 'dart:convert'; -``` -Now we’ll need to add a listview to our flutter app. - - -### Adding a ListView -Next, we’ll be adding a listview in our flutter app.If you don’t know how to create a listview in flutter,here you have the link to learn more about ListViews. -https://pusher.com/tutorials/flutter-listviews - - -Let's create the getList function that will show us the date or show us a message saying "Please Wait". - - -```ruby -Widget getList() { - if (data == null || data.length < 1) { - return Container( - child: Center( - child: Text("Please wait..."), - ), - ); - } - return ListView.separated( - itemCount: data?.length, - itemBuilder: (BuildContext context, int index) { - return getListItem(index); - }, -``` - -Just create a Text widget and add some styling as you like. - -## Conclusion - -This is a quick example of how to make an API call in Flutter. diff --git a/tdd-example/README.md b/tdd-example/README.md deleted file mode 100644 index a5c1640..0000000 --- a/tdd-example/README.md +++ /dev/null @@ -1 +0,0 @@ -GOTO: https://github.com/dwyl/flutter-counter-example From 15b2d6209ae3fc6a922b762cca68173f7086a0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Wed, 9 Nov 2022 11:09:04 +0000 Subject: [PATCH 16/33] feat: Deleting old project (it wasn't running), refactoring guides and starting a new one. Adding first steps on README. --- .DS_Store | Bin 8196 -> 8196 bytes README.md | 41 +- android/.gitignore | 7 - .../com/example/learn_flutter/MainActivity.kt | 12 - android/app/src/main/res/values/styles.xml | 8 - android/learn_flutter_android.iml | 29 - android/settings.gradle | 15 - demo_app/.gitignore | 44 ++ demo_app/.metadata | 45 ++ demo_app/README.md | 16 + demo_app/analysis_options.yaml | 29 + demo_app/android/.gitignore | 13 + .../android}/app/build.gradle | 28 +- .../app/src/debug}/AndroidManifest.xml | 5 +- .../android}/app/src/main/AndroidManifest.xml | 22 +- .../com/example/demo_app/MainActivity.kt | 6 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile}/AndroidManifest.xml | 5 +- {android => demo_app/android}/build.gradle | 8 +- .../android}/gradle.properties | 1 - .../gradle/wrapper/gradle-wrapper.properties | 3 +- demo_app/android/settings.gradle | 11 + {ios => demo_app/ios}/.gitignore | 2 + .../ios}/Flutter/AppFrameworkInfo.plist | 4 +- {ios => demo_app/ios}/Flutter/Debug.xcconfig | 0 .../ios}/Flutter/Release.xcconfig | 0 .../ios}/Runner.xcodeproj/project.pbxproj | 73 +-- .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 10 +- .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../ios}/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../Icon-App-1024x1024@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin .../Icon-App-83.5x83.5@2x.png | Bin .../LaunchImage.imageset/Contents.json | 0 .../LaunchImage.imageset/LaunchImage.png | Bin .../LaunchImage.imageset/LaunchImage@2x.png | Bin .../LaunchImage.imageset/LaunchImage@3x.png | Bin .../LaunchImage.imageset/README.md | 0 .../Runner/Base.lproj/LaunchScreen.storyboard | 0 .../ios}/Runner/Base.lproj/Main.storyboard | 0 {ios => demo_app/ios}/Runner/Info.plist | 8 +- .../ios}/Runner/Runner-Bridging-Header.h | 0 {lib => demo_app/lib}/main.dart | 18 +- demo_app/linux/.gitignore | 1 + demo_app/linux/CMakeLists.txt | 138 +++++ demo_app/linux/flutter/CMakeLists.txt | 88 +++ .../flutter/generated_plugin_registrant.cc | 11 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 23 + demo_app/linux/main.cc | 6 + demo_app/linux/my_application.cc | 104 ++++ demo_app/linux/my_application.h | 18 + demo_app/macos/.gitignore | 7 + demo_app/macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 10 + .../macos/Runner.xcodeproj/project.pbxproj | 572 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 +++ .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 8 + demo_app/macos/Runner/AppDelegate.swift | 9 + .../AppIcon.appiconset/Contents.json | 68 +++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes demo_app/macos/Runner/Base.lproj/MainMenu.xib | 343 +++++++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + demo_app/macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + demo_app/macos/Runner/Info.plist | 32 + demo_app/macos/Runner/MainFlutterWindow.swift | 15 + demo_app/macos/Runner/Release.entitlements | 8 + pubspec.yaml => demo_app/pubspec.yaml | 31 +- {test => demo_app/test}/widget_test.dart | 6 +- demo_app/web/favicon.png | Bin 0 -> 917 bytes {web => demo_app/web}/icons/Icon-192.png | Bin {web => demo_app/web}/icons/Icon-512.png | Bin demo_app/web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes demo_app/web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes demo_app/web/index.html | 58 ++ {web => demo_app/web}/manifest.json | 18 +- demo_app/windows/.gitignore | 17 + demo_app/windows/CMakeLists.txt | 101 ++++ demo_app/windows/flutter/CMakeLists.txt | 104 ++++ .../flutter/generated_plugin_registrant.cc | 11 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 23 + demo_app/windows/runner/CMakeLists.txt | 39 ++ demo_app/windows/runner/Runner.rc | 121 ++++ demo_app/windows/runner/flutter_window.cpp | 61 ++ demo_app/windows/runner/flutter_window.h | 33 + demo_app/windows/runner/main.cpp | 43 ++ demo_app/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes demo_app/windows/runner/runner.exe.manifest | 20 + demo_app/windows/runner/utils.cpp | 64 ++ demo_app/windows/runner/utils.h | 19 + demo_app/windows/runner/win32_window.cpp | 245 ++++++++ demo_app/windows/runner/win32_window.h | 98 +++ .../Flutter_TDD_Architecture_Course_notes.md | 0 .../login-firebase-tutorial.md | 0 .../webview-tutorial.md | 0 learn_flutter.iml | 18 - web/index.html | 20 - 137 files changed, 3121 insertions(+), 226 deletions(-) delete mode 100644 android/.gitignore delete mode 100644 android/app/src/main/kotlin/com/example/learn_flutter/MainActivity.kt delete mode 100644 android/app/src/main/res/values/styles.xml delete mode 100644 android/learn_flutter_android.iml delete mode 100644 android/settings.gradle create mode 100644 demo_app/.gitignore create mode 100644 demo_app/.metadata create mode 100644 demo_app/README.md create mode 100644 demo_app/analysis_options.yaml create mode 100644 demo_app/android/.gitignore rename {android => demo_app/android}/app/build.gradle (72%) rename {android/app/src/profile => demo_app/android/app/src/debug}/AndroidManifest.xml (53%) rename {android => demo_app/android}/app/src/main/AndroidManifest.xml (60%) create mode 100644 demo_app/android/app/src/main/kotlin/com/example/demo_app/MainActivity.kt create mode 100644 demo_app/android/app/src/main/res/drawable-v21/launch_background.xml rename {android => demo_app/android}/app/src/main/res/drawable/launch_background.xml (100%) rename {android => demo_app/android}/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename {android => demo_app/android}/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename {android => demo_app/android}/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {android => demo_app/android}/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {android => demo_app/android}/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) create mode 100644 demo_app/android/app/src/main/res/values-night/styles.xml create mode 100644 demo_app/android/app/src/main/res/values/styles.xml rename {android/app/src/debug => demo_app/android/app/src/profile}/AndroidManifest.xml (53%) rename {android => demo_app/android}/build.gradle (76%) rename {android => demo_app/android}/gradle.properties (78%) rename {android => demo_app/android}/gradle/wrapper/gradle-wrapper.properties (79%) create mode 100644 demo_app/android/settings.gradle rename {ios => demo_app/ios}/.gitignore (95%) rename {ios => demo_app/ios}/Flutter/AppFrameworkInfo.plist (91%) rename {ios => demo_app/ios}/Flutter/Debug.xcconfig (100%) rename {ios => demo_app/ios}/Flutter/Release.xcconfig (100%) rename {ios => demo_app/ios}/Runner.xcodeproj/project.pbxproj (85%) create mode 100644 demo_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 demo_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 demo_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename {ios => demo_app/ios}/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (95%) rename {ios/Runner.xcodeproj/project.xcworkspace => demo_app/ios/Runner.xcworkspace}/contents.xcworkspacedata (100%) create mode 100644 demo_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 demo_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename {ios => demo_app/ios}/Runner/AppDelegate.swift (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png (100%) rename {ios => demo_app/ios}/Runner/Assets.xcassets/LaunchImage.imageset/README.md (100%) rename {ios => demo_app/ios}/Runner/Base.lproj/LaunchScreen.storyboard (100%) rename {ios => demo_app/ios}/Runner/Base.lproj/Main.storyboard (100%) rename {ios => demo_app/ios}/Runner/Info.plist (87%) rename {ios => demo_app/ios}/Runner/Runner-Bridging-Header.h (100%) rename {lib => demo_app/lib}/main.dart (91%) create mode 100644 demo_app/linux/.gitignore create mode 100644 demo_app/linux/CMakeLists.txt create mode 100644 demo_app/linux/flutter/CMakeLists.txt create mode 100644 demo_app/linux/flutter/generated_plugin_registrant.cc create mode 100644 demo_app/linux/flutter/generated_plugin_registrant.h create mode 100644 demo_app/linux/flutter/generated_plugins.cmake create mode 100644 demo_app/linux/main.cc create mode 100644 demo_app/linux/my_application.cc create mode 100644 demo_app/linux/my_application.h create mode 100644 demo_app/macos/.gitignore create mode 100644 demo_app/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 demo_app/macos/Flutter/Flutter-Release.xcconfig create mode 100644 demo_app/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 demo_app/macos/Runner.xcodeproj/project.pbxproj create mode 100644 demo_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 demo_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename {ios => demo_app/macos}/Runner.xcworkspace/contents.xcworkspacedata (100%) create mode 100644 demo_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 demo_app/macos/Runner/AppDelegate.swift create mode 100644 demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 demo_app/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 demo_app/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 demo_app/macos/Runner/Configs/Debug.xcconfig create mode 100644 demo_app/macos/Runner/Configs/Release.xcconfig create mode 100644 demo_app/macos/Runner/Configs/Warnings.xcconfig create mode 100644 demo_app/macos/Runner/DebugProfile.entitlements create mode 100644 demo_app/macos/Runner/Info.plist create mode 100644 demo_app/macos/Runner/MainFlutterWindow.swift create mode 100644 demo_app/macos/Runner/Release.entitlements rename pubspec.yaml => demo_app/pubspec.yaml (63%) rename {test => demo_app/test}/widget_test.dart (84%) create mode 100644 demo_app/web/favicon.png rename {web => demo_app/web}/icons/Icon-192.png (100%) rename {web => demo_app/web}/icons/Icon-512.png (100%) create mode 100644 demo_app/web/icons/Icon-maskable-192.png create mode 100644 demo_app/web/icons/Icon-maskable-512.png create mode 100644 demo_app/web/index.html rename {web => demo_app/web}/manifest.json (53%) create mode 100644 demo_app/windows/.gitignore create mode 100644 demo_app/windows/CMakeLists.txt create mode 100644 demo_app/windows/flutter/CMakeLists.txt create mode 100644 demo_app/windows/flutter/generated_plugin_registrant.cc create mode 100644 demo_app/windows/flutter/generated_plugin_registrant.h create mode 100644 demo_app/windows/flutter/generated_plugins.cmake create mode 100644 demo_app/windows/runner/CMakeLists.txt create mode 100644 demo_app/windows/runner/Runner.rc create mode 100644 demo_app/windows/runner/flutter_window.cpp create mode 100644 demo_app/windows/runner/flutter_window.h create mode 100644 demo_app/windows/runner/main.cpp create mode 100644 demo_app/windows/runner/resource.h create mode 100644 demo_app/windows/runner/resources/app_icon.ico create mode 100644 demo_app/windows/runner/runner.exe.manifest create mode 100644 demo_app/windows/runner/utils.cpp create mode 100644 demo_app/windows/runner/utils.h create mode 100644 demo_app/windows/runner/win32_window.cpp create mode 100644 demo_app/windows/runner/win32_window.h rename Flutter_TDD_Architecture_Course_notes.md => guides/Flutter_TDD_Architecture_Course_notes.md (100%) rename login-firebase-tutorial => guides/login-firebase-tutorial.md (100%) rename webview-tutorial.md => guides/webview-tutorial.md (100%) delete mode 100644 learn_flutter.iml delete mode 100644 web/index.html diff --git a/.DS_Store b/.DS_Store index f4600ef171eebbb5d19987763f446acdb77deff0..e203579c5f21bf658bec0a17e113b4f66e4ee457 100644 GIT binary patch delta 397 zcmZp1XmOa}FUrNhz`)4BAi%&7&ydJaz);|slb^h?a2or>2Hwr=94s95AQc=8DGaF$ zxeWQps*-Z@lYnwStxV}aTJJv?09jbnCzTf$K-KR`D#*z!E-^5;#>m9X!pg?Z!Op?W z5gVM5UmjeNSW;T-lvorE;)Uer=OiUg<`%ZC=iubvj2DoquGTd)GSE>lv@kWQ)lsOn z1aVA^&1!2oIYgE9t%KsTb8_?Yd%$jBUy|B{c0wI3hpXj2S*9pln WZ!9^^w3%Jv8_VQQQRj^%N0bMHClzBi~0Dvzf{0l)xOfDjGTPlC{MgK_)Wdb(Sl zSa!zlD};p_TBxIwMujeSSco#;t)Jv!#2Cj2&_kEYXH6AnIO3|}fSHF&GN~Pp-yZ+? z+&N@t6~{@|Vx|x=O7d0Y`xt2yCR>Dvr*daPj5Q4P>P^(iEt|-`NykLQYY0j>jIzRa?vN>9sbs4wcjES}WossRr K=l>HoGJXN!AmG>l diff --git a/README.md b/README.md index 9e87fd5..1bf8935 100644 --- a/README.md +++ b/README.md @@ -1542,5 +1542,44 @@ And you're done! Congratulations, you just unit *and* integration tested your application. Awesome work! :tada: +# App demo πŸ“± + +We've learnt a lot about basic Flutter principles. There is no +better way of learning them by creating an app and applying them! +In this section, we'll walk you through to creating an +application that fetches information from a rest API, +lists them and allows the user to choose his favourites. + +Let's get cracking! + +## 0. Setting up a new project +In this walkthrough we are going to use Visual Studio Code. +We will assume you have this IDE installed, as well as the +`Flutter` and `Dart` extensions installed. If not, do so. + +extensions + +After restarting Visual Studio Code, let's create a new project! +Click on `View > Command Palette`, type `Flutter` and click on +`Flutter: New Project`. It will ask you for a name of the new project +- just type something like 'demo_app' - and then click `Enter` and your +project should start setting up! + +Let's run our newly created app. +On the bottom menu of Visual Studio Code, click on the device button +and you are shown a menu asking you to choose a device you want to run +the app from. I'll be going with iPhone 14 Pro Max. + +Bottom menu | After clicking, you are prompted with this menu +:-------------------------:|:-------------------------: +![](https://user-images.githubusercontent.com/17494745/200813538-ceb06084-95ed-492f-940e-27ceaf86c6da.png) | ![](https://user-images.githubusercontent.com/17494745/200813745-5c75d190-5306-4f7c-88da-cffea66d4a27.png) + +After setting up the device, the emulator should be shown. +After that, in Visual Studio Code, click on `Run > Start debugging`. +The build process will start and, after it is finished, +the app will start on the newly created emulator. +You should now see an "Hello World" app running. +Awesome! :tada: + +hello_world -## Fazer sample app simples \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index bc2100d..0000000 --- a/android/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java diff --git a/android/app/src/main/kotlin/com/example/learn_flutter/MainActivity.kt b/android/app/src/main/kotlin/com/example/learn_flutter/MainActivity.kt deleted file mode 100644 index 2fdc90e..0000000 --- a/android/app/src/main/kotlin/com/example/learn_flutter/MainActivity.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.learn_flutter - -import androidx.annotation.NonNull; -import io.flutter.embedding.android.FlutterActivity -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugins.GeneratedPluginRegistrant - -class MainActivity: FlutterActivity() { - override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { - GeneratedPluginRegistrant.registerWith(flutterEngine); - } -} diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml deleted file mode 100644 index 00fa441..0000000 --- a/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/android/learn_flutter_android.iml b/android/learn_flutter_android.iml deleted file mode 100644 index 1029d72..0000000 --- a/android/learn_flutter_android.iml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index 5a2f14f..0000000 --- a/android/settings.gradle +++ /dev/null @@ -1,15 +0,0 @@ -include ':app' - -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() - -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } -} - -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} diff --git a/demo_app/.gitignore b/demo_app/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/demo_app/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/demo_app/.metadata b/demo_app/.metadata new file mode 100644 index 0000000..2438210 --- /dev/null +++ b/demo_app/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: d9111f64021372856901a1fd5bfbc386cade3318 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d9111f64021372856901a1fd5bfbc386cade3318 + base_revision: d9111f64021372856901a1fd5bfbc386cade3318 + - platform: android + create_revision: d9111f64021372856901a1fd5bfbc386cade3318 + base_revision: d9111f64021372856901a1fd5bfbc386cade3318 + - platform: ios + create_revision: d9111f64021372856901a1fd5bfbc386cade3318 + base_revision: d9111f64021372856901a1fd5bfbc386cade3318 + - platform: linux + create_revision: d9111f64021372856901a1fd5bfbc386cade3318 + base_revision: d9111f64021372856901a1fd5bfbc386cade3318 + - platform: macos + create_revision: d9111f64021372856901a1fd5bfbc386cade3318 + base_revision: d9111f64021372856901a1fd5bfbc386cade3318 + - platform: web + create_revision: d9111f64021372856901a1fd5bfbc386cade3318 + base_revision: d9111f64021372856901a1fd5bfbc386cade3318 + - platform: windows + create_revision: d9111f64021372856901a1fd5bfbc386cade3318 + base_revision: d9111f64021372856901a1fd5bfbc386cade3318 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/demo_app/README.md b/demo_app/README.md new file mode 100644 index 0000000..1dde134 --- /dev/null +++ b/demo_app/README.md @@ -0,0 +1,16 @@ +# demo_app + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/demo_app/analysis_options.yaml b/demo_app/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/demo_app/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/demo_app/android/.gitignore b/demo_app/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/demo_app/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +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/demo_app/android/app/build.gradle similarity index 72% rename from android/app/build.gradle rename to demo_app/android/app/build.gradle index 5f4bb2e..ab4af1d 100644 --- a/android/app/build.gradle +++ b/demo_app/android/app/build.gradle @@ -26,24 +26,31 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion - 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.example.learn_flutter" - minSdkVersion 16 - targetSdkVersion 28 + applicationId "com.example.demo_app" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -61,7 +68,4 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } diff --git a/android/app/src/profile/AndroidManifest.xml b/demo_app/android/app/src/debug/AndroidManifest.xml similarity index 53% rename from android/app/src/profile/AndroidManifest.xml rename to demo_app/android/app/src/debug/AndroidManifest.xml index 0705650..22b7808 100644 --- a/android/app/src/profile/AndroidManifest.xml +++ b/demo_app/android/app/src/debug/AndroidManifest.xml @@ -1,6 +1,7 @@ - diff --git a/android/app/src/main/AndroidManifest.xml b/demo_app/android/app/src/main/AndroidManifest.xml similarity index 60% rename from android/app/src/main/AndroidManifest.xml rename to demo_app/android/app/src/main/AndroidManifest.xml index dbfd242..9a3af4d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/demo_app/android/app/src/main/AndroidManifest.xml @@ -1,21 +1,25 @@ - - + + + diff --git a/demo_app/android/app/src/main/kotlin/com/example/demo_app/MainActivity.kt b/demo_app/android/app/src/main/kotlin/com/example/demo_app/MainActivity.kt new file mode 100644 index 0000000..6a05513 --- /dev/null +++ b/demo_app/android/app/src/main/kotlin/com/example/demo_app/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.demo_app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/demo_app/android/app/src/main/res/drawable-v21/launch_background.xml b/demo_app/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/demo_app/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/demo_app/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from android/app/src/main/res/drawable/launch_background.xml rename to demo_app/android/app/src/main/res/drawable/launch_background.xml diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/demo_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to demo_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/demo_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to demo_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/demo_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to demo_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demo_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to demo_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demo_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to demo_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/demo_app/android/app/src/main/res/values-night/styles.xml b/demo_app/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/demo_app/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/demo_app/android/app/src/main/res/values/styles.xml b/demo_app/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/demo_app/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/debug/AndroidManifest.xml b/demo_app/android/app/src/profile/AndroidManifest.xml similarity index 53% rename from android/app/src/debug/AndroidManifest.xml rename to demo_app/android/app/src/profile/AndroidManifest.xml index 0705650..22b7808 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/demo_app/android/app/src/profile/AndroidManifest.xml @@ -1,6 +1,7 @@ - diff --git a/android/build.gradle b/demo_app/android/build.gradle similarity index 76% rename from android/build.gradle rename to demo_app/android/build.gradle index 3100ad2..83ae220 100644 --- a/android/build.gradle +++ b/demo_app/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.10' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -14,7 +14,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/android/gradle.properties b/demo_app/android/gradle.properties similarity index 78% rename from android/gradle.properties rename to demo_app/android/gradle.properties index 38c8d45..94adc3a 100644 --- a/android/gradle.properties +++ b/demo_app/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/demo_app/android/gradle/wrapper/gradle-wrapper.properties similarity index 79% rename from android/gradle/wrapper/gradle-wrapper.properties rename to demo_app/android/gradle/wrapper/gradle-wrapper.properties index 296b146..cb24abd 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/demo_app/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/demo_app/android/settings.gradle b/demo_app/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/demo_app/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/ios/.gitignore b/demo_app/ios/.gitignore similarity index 95% rename from ios/.gitignore rename to demo_app/ios/.gitignore index e96ef60..7a7f987 100644 --- a/ios/.gitignore +++ b/demo_app/ios/.gitignore @@ -1,3 +1,4 @@ +**/dgph *.mode1v3 *.mode2v3 *.moved-aside @@ -18,6 +19,7 @@ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig +Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ diff --git a/ios/Flutter/AppFrameworkInfo.plist b/demo_app/ios/Flutter/AppFrameworkInfo.plist similarity index 91% rename from ios/Flutter/AppFrameworkInfo.plist rename to demo_app/ios/Flutter/AppFrameworkInfo.plist index 6b4c0f7..9625e10 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/demo_app/ios/Flutter/AppFrameworkInfo.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) + en CFBundleExecutable App CFBundleIdentifier @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 11.0 diff --git a/ios/Flutter/Debug.xcconfig b/demo_app/ios/Flutter/Debug.xcconfig similarity index 100% rename from ios/Flutter/Debug.xcconfig rename to demo_app/ios/Flutter/Debug.xcconfig diff --git a/ios/Flutter/Release.xcconfig b/demo_app/ios/Flutter/Release.xcconfig similarity index 100% rename from ios/Flutter/Release.xcconfig rename to demo_app/ios/Flutter/Release.xcconfig diff --git a/ios/Runner.xcodeproj/project.pbxproj b/demo_app/ios/Runner.xcodeproj/project.pbxproj similarity index 85% rename from ios/Runner.xcodeproj/project.pbxproj rename to demo_app/ios/Runner.xcodeproj/project.pbxproj index 87497d7..1e301e0 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/demo_app/ios/Runner.xcodeproj/project.pbxproj @@ -3,17 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -26,8 +22,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -38,13 +32,11 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -57,8 +49,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -68,9 +58,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -102,7 +90,6 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, @@ -111,13 +98,6 @@ path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - ); - name = "Supporting Files"; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -147,7 +127,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -157,7 +137,7 @@ }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; + compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -201,7 +181,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -253,7 +233,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -293,7 +272,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -310,17 +289,12 @@ CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.learnFlutter; + PRODUCT_BUNDLE_IDENTIFIER = com.example.demoApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -330,7 +304,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -376,7 +349,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -386,7 +359,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -426,11 +398,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -444,17 +417,12 @@ CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.learnFlutter; + PRODUCT_BUNDLE_IDENTIFIER = com.example.demoApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -471,17 +439,12 @@ CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.learnFlutter; + PRODUCT_BUNDLE_IDENTIFIER = com.example.demoApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/demo_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/demo_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/demo_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/demo_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demo_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/demo_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demo_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/demo_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/demo_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demo_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 95% rename from ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to demo_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140c..c87d15a 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demo_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + - - + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demo_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/demo_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/demo_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/demo_app/ios/Runner/AppDelegate.swift similarity index 100% rename from ios/Runner/AppDelegate.swift rename to demo_app/ios/Runner/AppDelegate.swift diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to demo_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/demo_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to demo_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/demo_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to demo_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/demo_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to demo_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/demo_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to demo_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/demo_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to demo_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/demo_app/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from ios/Runner/Base.lproj/LaunchScreen.storyboard rename to demo_app/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/ios/Runner/Base.lproj/Main.storyboard b/demo_app/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from ios/Runner/Base.lproj/Main.storyboard rename to demo_app/ios/Runner/Base.lproj/Main.storyboard diff --git a/ios/Runner/Info.plist b/demo_app/ios/Runner/Info.plist similarity index 87% rename from ios/Runner/Info.plist rename to demo_app/ios/Runner/Info.plist index 2667988..dcae618 100644 --- a/ios/Runner/Info.plist +++ b/demo_app/ios/Runner/Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Demo App CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -11,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - learn_flutter + demo_app CFBundlePackageType APPL CFBundleShortVersionString @@ -41,5 +43,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/ios/Runner/Runner-Bridging-Header.h b/demo_app/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from ios/Runner/Runner-Bridging-Header.h rename to demo_app/ios/Runner/Runner-Bridging-Header.h diff --git a/lib/main.dart b/demo_app/lib/main.dart similarity index 91% rename from lib/main.dart rename to demo_app/lib/main.dart index 5a7af45..e016029 100644 --- a/lib/main.dart +++ b/demo_app/lib/main.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; -void main() => runApp(MyApp()); +void main() { + runApp(const MyApp()); +} class MyApp extends StatelessWidget { + const MyApp({super.key}); + // This widget is the root of your application. @override Widget build(BuildContext context) { @@ -20,13 +24,13 @@ class MyApp extends StatelessWidget { // is not restarted. primarySwatch: Colors.blue, ), - home: MyHomePage(title: 'Flutter Demo Home Page'), + home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + const MyHomePage({super.key, required this.title}); // This widget is the home page of your application. It is stateful, meaning // that it has a State object (defined below) that contains fields that affect @@ -40,7 +44,7 @@ class MyHomePage extends StatefulWidget { final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { @@ -91,12 +95,12 @@ class _MyHomePageState extends State { // horizontal). mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( + const Text( 'You have pushed the button this many times:', ), Text( '$_counter', - style: Theme.of(context).textTheme.display1, + style: Theme.of(context).textTheme.headline4, ), ], ), @@ -104,7 +108,7 @@ class _MyHomePageState extends State { floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', - child: Icon(Icons.add), + child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } diff --git a/demo_app/linux/.gitignore b/demo_app/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/demo_app/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/demo_app/linux/CMakeLists.txt b/demo_app/linux/CMakeLists.txt new file mode 100644 index 0000000..6efd888 --- /dev/null +++ b/demo_app/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "demo_app") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.demo_app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/demo_app/linux/flutter/CMakeLists.txt b/demo_app/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/demo_app/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/demo_app/linux/flutter/generated_plugin_registrant.cc b/demo_app/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/demo_app/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/demo_app/linux/flutter/generated_plugin_registrant.h b/demo_app/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/demo_app/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/demo_app/linux/flutter/generated_plugins.cmake b/demo_app/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2e1de87 --- /dev/null +++ b/demo_app/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/demo_app/linux/main.cc b/demo_app/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/demo_app/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/demo_app/linux/my_application.cc b/demo_app/linux/my_application.cc new file mode 100644 index 0000000..c253057 --- /dev/null +++ b/demo_app/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "demo_app"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "demo_app"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/demo_app/linux/my_application.h b/demo_app/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/demo_app/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/demo_app/macos/.gitignore b/demo_app/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/demo_app/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/demo_app/macos/Flutter/Flutter-Debug.xcconfig b/demo_app/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/demo_app/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/demo_app/macos/Flutter/Flutter-Release.xcconfig b/demo_app/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/demo_app/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/demo_app/macos/Flutter/GeneratedPluginRegistrant.swift b/demo_app/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..cccf817 --- /dev/null +++ b/demo_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,10 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { +} diff --git a/demo_app/macos/Runner.xcodeproj/project.pbxproj b/demo_app/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..db71855 --- /dev/null +++ b/demo_app/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,572 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* demo_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "demo_app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* demo_app.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* demo_app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/demo_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demo_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/demo_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demo_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demo_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..6977e7e --- /dev/null +++ b/demo_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/demo_app/macos/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from ios/Runner.xcworkspace/contents.xcworkspacedata rename to demo_app/macos/Runner.xcworkspace/contents.xcworkspacedata diff --git a/demo_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demo_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/demo_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demo_app/macos/Runner/AppDelegate.swift b/demo_app/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..d53ef64 --- /dev/null +++ b/demo_app/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 zcmaiYXH?Tqu=Xz`p-L#B_gI#0we$cm_HcmYFP$?wjD#BaCN4mzC5#`>w9y6=ThxrYZc0WPXprg zYjB`UsV}0=eUtY$(P6YW}npdd;%9pi?zS3k-nqCob zSX_AQEf|=wYT3r?f!*Yt)ar^;l3Sro{z(7deUBPd2~(SzZ-s@0r&~Km2S?8r##9-< z)2UOSVaHqq6}%sA9Ww;V2LG=PnNAh6mA2iWOuV7T_lRDR z&N8-eN=U)-T|;wo^Wv=34wtV0g}sAAe}`Ph@~!|<;z7*K8(qkX0}o=!(+N*UWrkEja*$_H6mhK1u{P!AC39} z|3+Z(mAOq#XRYS)TLoHv<)d%$$I@+x+2)V{@o~~J-!YUI-Q9%!Ldi4Op&Lw&B>jj* zwAgC#Y>gbIqv!d|J5f!$dbCXoq(l3GR(S>(rtZ~Z*agXMMKN!@mWT_vmCbSd3dUUm z4M&+gz?@^#RRGal%G3dDvj7C5QTb@9+!MG+>0dcjtZEB45c+qx*c?)d<%htn1o!#1 zpIGonh>P1LHu3s)fGFF-qS}AXjW|M*2Xjkh7(~r(lN=o#mBD9?jt74=Rz85I4Nfx_ z7Z)q?!};>IUjMNM6ee2Thq7))a>My?iWFxQ&}WvsFP5LP+iGz+QiYek+K1`bZiTV- zHHYng?ct@Uw5!gquJ(tEv1wTrRR7cemI>aSzLI^$PxW`wL_zt@RSfZ1M3c2sbebM* ze0=;sy^!90gL~YKISz*x;*^~hcCoO&CRD)zjT(A2b_uRue=QXFe5|!cf0z1m!iwv5GUnLw9Dr*Ux z)3Lc!J@Ei;&&yxGpf2kn@2wJ2?t6~obUg;?tBiD#uo$SkFIasu+^~h33W~`r82rSa ztyE;ehFjC2hjpJ-e__EH&z?!~>UBb=&%DS>NT)1O3Isn-!SElBV2!~m6v0$vx^a<@ISutdTk1@?;i z<8w#b-%|a#?e5(n@7>M|v<<0Kpg?BiHYMRe!3Z{wYc2hN{2`6(;q`9BtXIhVq6t~KMH~J0~XtUuT06hL8c1BYZWhN zk4F2I;|za*R{ToHH2L?MfRAm5(i1Ijw;f+0&J}pZ=A0;A4M`|10ZskA!a4VibFKn^ zdVH4OlsFV{R}vFlD~aA4xxSCTTMW@Gws4bFWI@xume%smAnuJ0b91QIF?ZV!%VSRJ zO7FmG!swKO{xuH{DYZ^##gGrXsUwYfD0dxXX3>QmD&`mSi;k)YvEQX?UyfIjQeIm! z0ME3gmQ`qRZ;{qYOWt}$-mW*>D~SPZKOgP)T-Sg%d;cw^#$>3A9I(%#vsTRQe%moT zU`geRJ16l>FV^HKX1GG7fR9AT((jaVb~E|0(c-WYQscVl(z?W!rJp`etF$dBXP|EG z=WXbcZ8mI)WBN>3<@%4eD597FD5nlZajwh8(c$lum>yP)F}=(D5g1-WVZRc)(!E3} z-6jy(x$OZOwE=~{EQS(Tp`yV2&t;KBpG*XWX!yG+>tc4aoxbXi7u@O*8WWFOxUjcq z^uV_|*818$+@_{|d~VOP{NcNi+FpJ9)aA2So<7sB%j`$Prje&auIiTBb{oD7q~3g0 z>QNIwcz(V-y{Ona?L&=JaV5`o71nIsWUMA~HOdCs10H+Irew#Kr(2cn>orG2J!jvP zqcVX0OiF}c<)+5&p}a>_Uuv)L_j}nqnJ5a?RPBNi8k$R~zpZ33AA4=xJ@Z($s3pG9 zkURJY5ZI=cZGRt_;`hs$kE@B0FrRx(6K{`i1^*TY;Vn?|IAv9|NrN*KnJqO|8$e1& zb?OgMV&q5|w7PNlHLHF) zB+AK#?EtCgCvwvZ6*u|TDhJcCO+%I^@Td8CR}+nz;OZ*4Dn?mSi97m*CXXc=};!P`B?}X`F-B5v-%ACa8fo0W++j&ztmqK z;&A)cT4ob9&MxpQU41agyMU8jFq~RzXOAsy>}hBQdFVL%aTn~M>5t9go2j$i9=(rZ zADmVj;Qntcr3NIPPTggpUxL_z#5~C!Gk2Rk^3jSiDqsbpOXf^f&|h^jT4|l2ehPat zb$<*B+x^qO8Po2+DAmrQ$Zqc`1%?gp*mDk>ERf6I|42^tjR6>}4`F_Mo^N(~Spjcg z_uY$}zui*PuDJjrpP0Pd+x^5ds3TG#f?57dFL{auS_W8|G*o}gcnsKYjS6*t8VI<) zcjqTzW(Hk*t-Qhq`Xe+x%}sxXRerScbPGv8hlJ;CnU-!Nl=# zR=iTFf9`EItr9iAlAGi}i&~nJ-&+)Y| zMZigh{LXe)uR+4D_Yb+1?I93mHQ5{pId2Fq%DBr7`?ipi;CT!Q&|EO3gH~7g?8>~l zT@%*5BbetH)~%TrAF1!-!=)`FIS{^EVA4WlXYtEy^|@y@yr!C~gX+cp2;|O4x1_Ol z4fPOE^nj(}KPQasY#U{m)}TZt1C5O}vz`A|1J!-D)bR%^+=J-yJsQXDzFiqb+PT0! zIaDWWU(AfOKlSBMS};3xBN*1F2j1-_=%o($ETm8@oR_NvtMDVIv_k zlnNBiHU&h8425{MCa=`vb2YP5KM7**!{1O>5Khzu+5OVGY;V=Vl+24fOE;tMfujoF z0M``}MNnTg3f%Uy6hZi$#g%PUA_-W>uVCYpE*1j>U8cYP6m(>KAVCmbsDf39Lqv0^ zt}V6FWjOU@AbruB7MH2XqtnwiXS2scgjVMH&aF~AIduh#^aT1>*V>-st8%=Kk*{bL zzbQcK(l2~)*A8gvfX=RPsNnjfkRZ@3DZ*ff5rmx{@iYJV+a@&++}ZW+za2fU>&(4y`6wgMpQGG5Ah(9oGcJ^P(H< zvYn5JE$2B`Z7F6ihy>_49!6}(-)oZ(zryIXt=*a$bpIw^k?>RJ2 zQYr>-D#T`2ZWDU$pM89Cl+C<;J!EzHwn(NNnWpYFqDDZ_*FZ{9KQRcSrl5T>dj+eA zi|okW;6)6LR5zebZJtZ%6Gx8^=2d9>_670!8Qm$wd+?zc4RAfV!ZZ$jV0qrv(D`db zm_T*KGCh3CJGb(*X6nXzh!h9@BZ-NO8py|wG8Qv^N*g?kouH4%QkPU~Vizh-D3<@% zGomx%q42B7B}?MVdv1DFb!axQ73AUxqr!yTyFlp%Z1IAgG49usqaEbI_RnbweR;Xs zpJq7GKL_iqi8Md?f>cR?^0CA+Uk(#mTlGdZbuC*$PrdB$+EGiW**=$A3X&^lM^K2s zzwc3LtEs5|ho z2>U(-GL`}eNgL-nv3h7E<*<>C%O^=mmmX0`jQb6$mP7jUKaY4je&dCG{x$`0=_s$+ zSpgn!8f~ya&U@c%{HyrmiW2&Wzc#Sw@+14sCpTWReYpF9EQ|7vF*g|sqG3hx67g}9 zwUj5QP2Q-(KxovRtL|-62_QsHLD4Mu&qS|iDp%!rs(~ah8FcrGb?Uv^Qub5ZT_kn%I^U2rxo1DDpmN@8uejxik`DK2~IDi1d?%~pR7i#KTS zA78XRx<(RYO0_uKnw~vBKi9zX8VnjZEi?vD?YAw}y+)wIjIVg&5(=%rjx3xQ_vGCy z*&$A+bT#9%ZjI;0w(k$|*x{I1c!ECMus|TEA#QE%#&LxfGvijl7Ih!B2 z6((F_gwkV;+oSKrtr&pX&fKo3s3`TG@ye+k3Ov)<#J|p8?vKh@<$YE@YIU1~@7{f+ zydTna#zv?)6&s=1gqH<-piG>E6XW8ZI7&b@-+Yk0Oan_CW!~Q2R{QvMm8_W1IV8<+ zQTyy=(Wf*qcQubRK)$B;QF}Y>V6d_NM#=-ydM?%EPo$Q+jkf}*UrzR?Nsf?~pzIj$ z<$wN;7c!WDZ(G_7N@YgZ``l;_eAd3+;omNjlpfn;0(B7L)^;;1SsI6Le+c^ULe;O@ zl+Z@OOAr4$a;=I~R0w4jO`*PKBp?3K+uJ+Tu8^%i<_~bU!p%so z^sjol^slR`W@jiqn!M~eClIIl+`A5%lGT{z^mRbpv}~AyO%R*jmG_Wrng{B9TwIuS z0!@fsM~!57K1l0%{yy(#no}roy#r!?0wm~HT!vLDfEBs9x#`9yCKgufm0MjVRfZ=f z4*ZRc2Lgr(P+j2zQE_JzYmP0*;trl7{*N341Cq}%^M^VC3gKG-hY zmPT>ECyrhIoFhnMB^qpdbiuI}pk{qPbK^}0?Rf7^{98+95zNq6!RuV_zAe&nDk0;f zez~oXlE5%ve^TmBEt*x_X#fs(-En$jXr-R4sb$b~`nS=iOy|OVrph(U&cVS!IhmZ~ zKIRA9X%Wp1J=vTvHZ~SDe_JXOe9*fa zgEPf;gD^|qE=dl>Qkx3(80#SE7oxXQ(n4qQ#by{uppSKoDbaq`U+fRqk0BwI>IXV3 zD#K%ASkzd7u>@|pA=)Z>rQr@dLH}*r7r0ng zxa^eME+l*s7{5TNu!+bD{Pp@2)v%g6^>yj{XP&mShhg9GszNu4ITW=XCIUp2Xro&1 zg_D=J3r)6hp$8+94?D$Yn2@Kp-3LDsci)<-H!wCeQt$e9Jk)K86hvV^*Nj-Ea*o;G zsuhRw$H{$o>8qByz1V!(yV{p_0X?Kmy%g#1oSmlHsw;FQ%j9S#}ha zm0Nx09@jmOtP8Q+onN^BAgd8QI^(y!n;-APUpo5WVdmp8!`yKTlF>cqn>ag`4;o>i zl!M0G-(S*fm6VjYy}J}0nX7nJ$h`|b&KuW4d&W5IhbR;-)*9Y0(Jj|@j`$xoPQ=Cl literal 0 HcmV?d00001 diff --git a/demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 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 literal 0 HcmV?d00001 diff --git a/demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/demo_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo_app/macos/Runner/Configs/AppInfo.xcconfig b/demo_app/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..49e4561 --- /dev/null +++ b/demo_app/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = demo_app + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.demoApp + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright Β© 2022 com.example. All rights reserved. diff --git a/demo_app/macos/Runner/Configs/Debug.xcconfig b/demo_app/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/demo_app/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/demo_app/macos/Runner/Configs/Release.xcconfig b/demo_app/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/demo_app/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/demo_app/macos/Runner/Configs/Warnings.xcconfig b/demo_app/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/demo_app/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/demo_app/macos/Runner/DebugProfile.entitlements b/demo_app/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/demo_app/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/demo_app/macos/Runner/Info.plist b/demo_app/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/demo_app/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/demo_app/macos/Runner/MainFlutterWindow.swift b/demo_app/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..2722837 --- /dev/null +++ b/demo_app/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/demo_app/macos/Runner/Release.entitlements b/demo_app/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/demo_app/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/pubspec.yaml b/demo_app/pubspec.yaml similarity index 63% rename from pubspec.yaml rename to demo_app/pubspec.yaml index 23c5b40..ee57297 100644 --- a/pubspec.yaml +++ b/demo_app/pubspec.yaml @@ -1,6 +1,10 @@ -name: learn_flutter +name: demo_app description: A new Flutter project. +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. @@ -8,31 +12,46 @@ description: A new Flutter project. # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: '>=2.18.2 <3.0.0' +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter + # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 + cupertino_icons: ^1.0.2 dev_dependencies: flutter_test: sdk: flutter + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec -# The following section is specific to Flutter. +# The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is @@ -46,7 +65,7 @@ flutter: # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. + # https://flutter.dev/assets-and-images/#resolution-aware # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages diff --git a/test/widget_test.dart b/demo_app/test/widget_test.dart similarity index 84% rename from test/widget_test.dart rename to demo_app/test/widget_test.dart index 97eee0d..40adf93 100644 --- a/test/widget_test.dart +++ b/demo_app/test/widget_test.dart @@ -1,19 +1,19 @@ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll +// utility in the flutter_test package. For example, you can send tap and scroll // 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:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:learn_flutter/main.dart'; +import 'package:demo_app/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); + await tester.pumpWidget(const MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); diff --git a/demo_app/web/favicon.png b/demo_app/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/web/icons/Icon-192.png b/demo_app/web/icons/Icon-192.png similarity index 100% rename from web/icons/Icon-192.png rename to demo_app/web/icons/Icon-192.png diff --git a/web/icons/Icon-512.png b/demo_app/web/icons/Icon-512.png similarity index 100% rename from web/icons/Icon-512.png rename to demo_app/web/icons/Icon-512.png diff --git a/demo_app/web/icons/Icon-maskable-192.png b/demo_app/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/demo_app/web/icons/Icon-maskable-512.png b/demo_app/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 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 MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/demo_app/web/index.html b/demo_app/web/index.html new file mode 100644 index 0000000..3c31370 --- /dev/null +++ b/demo_app/web/index.html @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + demo_app + + + + + + + + + + diff --git a/web/manifest.json b/demo_app/web/manifest.json similarity index 53% rename from web/manifest.json rename to demo_app/web/manifest.json index 9c1ca76..5f80e50 100644 --- a/web/manifest.json +++ b/demo_app/web/manifest.json @@ -1,8 +1,8 @@ { - "name": "learn_flutter", - "short_name": "learn_flutter", + "name": "demo_app", + "short_name": "demo_app", "start_url": ".", - "display": "minimal-ui", + "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", @@ -18,6 +18,18 @@ "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" } ] } diff --git a/demo_app/windows/.gitignore b/demo_app/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/demo_app/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/demo_app/windows/CMakeLists.txt b/demo_app/windows/CMakeLists.txt new file mode 100644 index 0000000..01b6591 --- /dev/null +++ b/demo_app/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(demo_app LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "demo_app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/demo_app/windows/flutter/CMakeLists.txt b/demo_app/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..930d207 --- /dev/null +++ b/demo_app/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/demo_app/windows/flutter/generated_plugin_registrant.cc b/demo_app/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..8b6d468 --- /dev/null +++ b/demo_app/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/demo_app/windows/flutter/generated_plugin_registrant.h b/demo_app/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/demo_app/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/demo_app/windows/flutter/generated_plugins.cmake b/demo_app/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..b93c4c3 --- /dev/null +++ b/demo_app/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/demo_app/windows/runner/CMakeLists.txt b/demo_app/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..17411a8 --- /dev/null +++ b/demo_app/windows/runner/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/demo_app/windows/runner/Runner.rc b/demo_app/windows/runner/Runner.rc new file mode 100644 index 0000000..f3ead6c --- /dev/null +++ b/demo_app/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "demo_app" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "demo_app" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "demo_app.exe" "\0" + VALUE "ProductName", "demo_app" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/demo_app/windows/runner/flutter_window.cpp b/demo_app/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..b43b909 --- /dev/null +++ b/demo_app/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/demo_app/windows/runner/flutter_window.h b/demo_app/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/demo_app/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/demo_app/windows/runner/main.cpp b/demo_app/windows/runner/main.cpp new file mode 100644 index 0000000..817450c --- /dev/null +++ b/demo_app/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"demo_app", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/demo_app/windows/runner/resource.h b/demo_app/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/demo_app/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/demo_app/windows/runner/resources/app_icon.ico b/demo_app/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/demo_app/windows/runner/runner.exe.manifest b/demo_app/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..a42ea76 --- /dev/null +++ b/demo_app/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/demo_app/windows/runner/utils.cpp b/demo_app/windows/runner/utils.cpp new file mode 100644 index 0000000..f5bf9fa --- /dev/null +++ b/demo_app/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/demo_app/windows/runner/utils.h b/demo_app/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/demo_app/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/demo_app/windows/runner/win32_window.cpp b/demo_app/windows/runner/win32_window.cpp new file mode 100644 index 0000000..c10f08d --- /dev/null +++ b/demo_app/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/demo_app/windows/runner/win32_window.h b/demo_app/windows/runner/win32_window.h new file mode 100644 index 0000000..17ba431 --- /dev/null +++ b/demo_app/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/Flutter_TDD_Architecture_Course_notes.md b/guides/Flutter_TDD_Architecture_Course_notes.md similarity index 100% rename from Flutter_TDD_Architecture_Course_notes.md rename to guides/Flutter_TDD_Architecture_Course_notes.md diff --git a/login-firebase-tutorial b/guides/login-firebase-tutorial.md similarity index 100% rename from login-firebase-tutorial rename to guides/login-firebase-tutorial.md diff --git a/webview-tutorial.md b/guides/webview-tutorial.md similarity index 100% rename from webview-tutorial.md rename to guides/webview-tutorial.md diff --git a/learn_flutter.iml b/learn_flutter.iml deleted file mode 100644 index e5c8371..0000000 --- a/learn_flutter.iml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/index.html b/web/index.html deleted file mode 100644 index 407c0c3..0000000 --- a/web/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - learn_flutter - - - - - - From 573a1e41daaab82b57b2b13df088c05bf910f427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Wed, 9 Nov 2022 13:06:13 +0000 Subject: [PATCH 17/33] feat: 1. Project structure. --- README.md | 214 ++++++++++++++++++++ demo_app/lib/main.dart | 65 +++--- demo_app/lib/models/todo.dart | 22 ++ demo_app/lib/repository/todoRepository.dart | 26 +++ demo_app/lib/services/todoService.dart | 12 ++ demo_app/pubspec.yaml | 1 + 6 files changed, 298 insertions(+), 42 deletions(-) create mode 100644 demo_app/lib/models/todo.dart create mode 100644 demo_app/lib/repository/todoRepository.dart create mode 100644 demo_app/lib/services/todoService.dart diff --git a/README.md b/README.md index 1bf8935..73cb652 100644 --- a/README.md +++ b/README.md @@ -1583,3 +1583,217 @@ Awesome! :tada: hello_world +## 1. Project structure +We could implement a really simple project structure for this demo. +But, just for learning purposes, let's implement a structure that is +divided into four layers: + +- [**presentation**](https://codewithandrea.com/articles/flutter-presentation-layer/): +consisting of widgets, states (either local or shared) and controllers. +- [**application**](https://codewithandrea.com/articles/flutter-app-architecture-application-layer/): +in this layer we will have *services*, which will fetch data from the *data layer*. +- [**domain layer**](https://codewithandrea.com/articles/flutter-app-architecture-domain-model/): +where we define domain classes for the business logic. +- [**data**](https://codewithandrea.com/articles/flutter-repository-pattern/): +our data sources and repositories. We will interact with APIs here. + + +![structure](https://codewithandrea.com/articles/flutter-project-structure/images/flutter-app-architecture.webp) + +> This structure borrows many concepts from +[DDD (Domain-driven-design)](https://en.wikipedia.org/wiki/Domain-driven_design), +where the codebase is modeled and implemented according +to domain logic and concepts. + +We will simplify these four layers because it is a small project. +But if you were to work in a corporate environmnent, you would be dealing +with various APIs, data sources and a large amount of models. +This structure makes it much easier to maintain code at a larger scale. +Although we might be breaking the [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it) +principle here, this is just to show how to structure your code +in a maintainable manner. + +Let's start! Firstly create the following folder structure. + +``` +lib + - models + todo.dart + - repository + todoRepository.dart + - services + todoService.dart + main.dart +``` + +In the `lib/models/todo.dart` file, add the following piece of code. + +```dart +class Todo { + final int userId; + final int id; + final String title; + final bool completed; + + const Todo({ + required this.userId, + required this.id, + required this.title, + required this.completed, + }); + + factory Todo.fromJson(Map json) { + return Todo( + userId: json['userId'], + id: json['id'], + title: json['title'], + completed: json['completed'], + ); + } +} +``` + +This should be nothing new to you. We declared +each member field and added a function that +parses a JSON object to the class. + +Next up, let's head to the repository file. Firstly, +run `flutter pub add http` +to install the `http` package, +as we are going to need it to fetch data from a third-party API. + +After that, let's create the `lib/repository/todoRepository.dart` file. + +```dart +import 'dart:convert'; + +import 'package:demo_app/models/todo.dart'; +import 'package:http/http.dart' as http; + +abstract class TodoRepository { + Future> getTodos(); +} + +class HTTPTodoRepository implements TodoRepository { + @override + Future> getTodos() async { + final response = + await http.get(Uri.parse("https://jsonplaceholder.typicode.com/todos")); + + if (response.statusCode == 200) { + Iterable l = json.decode(response.body); + List todos = + List.from(l.map((model) => Todo.fromJson(model))); + + return todos; + } else { + throw Exception('Failed to load Todo\'s.'); + } + } +} +``` + +Here, we are creating an `abstract` class, which will serve +as an interface for creating the `HTTPTodoRepository` class. +The class, since implements the `TodoRepository` abstract class, +will have to implement the `getTodos()` function. +In this function, we will call an API which returns an array of todos. +In case the call is successful, we parse each decoded json object +and convert it to a `Todo` object. + +Now let's go and implement the `lib/services/todoService.dart` file. + +```dart +import 'package:demo_app/models/todo.dart'; +import 'package:demo_app/repository/todoRepository.dart'; + +class TodoService { + late final TodoRepository todoRepository; + + TodoService() { + todoRepository = HTTPTodoRepository(); + } + + Future> getTodos() => todoRepository.getTodos(); +} +``` + +In this class, we initialize it by creating a `TodoRepository`. +We use this field member in the `getTodos()` function, +which in turn, calls the `TodoRepository's` function to fetch +the todos list. + +You might be asking yourself: "Well, mate, that's a lot of work +for just a simple fetching function, isn't it?". +Well, in this case, you'd be right. But we're just learning a +maintainable way of structuring our code. +This service might (and *is*, in this case) redudant. +But imagine if we have widgets that necessitate objects +that stem from various data sources. +It will be *the service's just* to fetch whatever data is needed +from each repository, compile it and give it to the widget. + +Let's continue. In the `main.dart` file, let's fetch the todo list +and show the first one, just to check that everything works. +Import the service and the models. + +```dart +import 'package:demo_app/models/todo.dart'; +import 'services/todoService.dart'; +``` + +and then change the `_MyHomePageState` class, like so. + +```dart +class _MyHomePageState extends State { + late Future> futureTodosList; + + @override + void initState() { + super.initState(); + futureTodosList = TodoService().getTodos(); + } + + @override + Widget build(BuildContext context) { + // This method is rerun every time setState is called, for instance as done + // by the _incrementCounter method above. + // + // The Flutter framework has been optimized to make rerunning build methods + // fast, so that you can just rebuild anything that needs updating rather + // than having to individually change instances of widgets. + return Scaffold( + appBar: AppBar( + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. + title: Text(widget.title), + ), + body: Center( + // Center is a layout widget. It takes a single child and positions it + // in the middle of the parent. + child: FutureBuilder>( + future: futureTodosList, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text(snapshot.data![0].title); + } else if (snapshot.hasError) { + return Text('${snapshot.error}'); + } + + // By default, show a loading spinner. + return const CircularProgressIndicator(); + }, + ), + ), + ) + ; + } +} +``` + +If you re-run the app, you should see something like this. +It is displaying the first todo title from the fetched list. +Hurray, we just set up all the data we need! +Now it's just about making it pretty :sparkles:. + + diff --git a/demo_app/lib/main.dart b/demo_app/lib/main.dart index e016029..bbb7225 100644 --- a/demo_app/lib/main.dart +++ b/demo_app/lib/main.dart @@ -1,5 +1,8 @@ +import 'package:demo_app/models/todo.dart'; import 'package:flutter/material.dart'; +import 'services/todoService.dart'; + void main() { runApp(const MyApp()); } @@ -48,17 +51,12 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - int _counter = 0; + late Future> futureTodosList; - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); + @override + void initState() { + super.initState(); + futureTodosList = TodoService().getTodos(); } @override @@ -78,38 +76,21 @@ class _MyHomePageState extends State { body: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Invoke "debug painting" (press "p" in the console, choose the - // "Toggle Debug Paint" action from the Flutter Inspector in Android - // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) - // to see the wireframe for each widget. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headline4, - ), - ], + child: FutureBuilder>( + future: futureTodosList, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text(snapshot.data![0].title); + } else if (snapshot.hasError) { + return Text('${snapshot.error}'); + } + + // By default, show a loading spinner. + return const CircularProgressIndicator(); + }, + ), ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. - ); + ) + ; } } diff --git a/demo_app/lib/models/todo.dart b/demo_app/lib/models/todo.dart new file mode 100644 index 0000000..01a271c --- /dev/null +++ b/demo_app/lib/models/todo.dart @@ -0,0 +1,22 @@ +class Todo { + final int userId; + final int id; + final String title; + final bool completed; + + const Todo({ + required this.userId, + required this.id, + required this.title, + required this.completed, + }); + + factory Todo.fromJson(Map json) { + return Todo( + userId: json['userId'], + id: json['id'], + title: json['title'], + completed: json['completed'], + ); + } +} diff --git a/demo_app/lib/repository/todoRepository.dart b/demo_app/lib/repository/todoRepository.dart new file mode 100644 index 0000000..a8a8c66 --- /dev/null +++ b/demo_app/lib/repository/todoRepository.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; + +import 'package:demo_app/models/todo.dart'; +import 'package:http/http.dart' as http; + +abstract class TodoRepository { + Future> getTodos(); +} + +class HTTPTodoRepository implements TodoRepository { + @override + Future> getTodos() async { + final response = + await http.get(Uri.parse("https://jsonplaceholder.typicode.com/todos")); + + if (response.statusCode == 200) { + Iterable l = json.decode(response.body); + List todos = + List.from(l.map((model) => Todo.fromJson(model))); + + return todos; + } else { + throw Exception('Failed to load Todo\'s.'); + } + } +} diff --git a/demo_app/lib/services/todoService.dart b/demo_app/lib/services/todoService.dart new file mode 100644 index 0000000..3f00e65 --- /dev/null +++ b/demo_app/lib/services/todoService.dart @@ -0,0 +1,12 @@ +import 'package:demo_app/models/todo.dart'; +import 'package:demo_app/repository/todoRepository.dart'; + +class TodoService { + late final TodoRepository todoRepository; + + TodoService() { + todoRepository = HTTPTodoRepository(); + } + + Future> getTodos() => todoRepository.getTodos(); +} diff --git a/demo_app/pubspec.yaml b/demo_app/pubspec.yaml index ee57297..476d762 100644 --- a/demo_app/pubspec.yaml +++ b/demo_app/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 + http: ^0.13.5 dev_dependencies: flutter_test: From b90c4861b6facf7d473d803efe868c78c05bbe47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Wed, 9 Nov 2022 14:07:49 +0000 Subject: [PATCH 18/33] feat: 2. Creating list of todos. --- README.md | 82 ++++++++++++++++++++++++++++++++++++++++++ demo_app/lib/main.dart | 52 +++++++++++++++++++-------- 2 files changed, 120 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 73cb652..788f578 100644 --- a/README.md +++ b/README.md @@ -1793,7 +1793,89 @@ class _MyHomePageState extends State { If you re-run the app, you should see something like this. It is displaying the first todo title from the fetched list. +We used the `FutureBuilder` class to indicate +that the data residing within will come at a later stage - +the todo list. If the data comes, we show the first todo title. +And we did all this in a `StatefulWidget`, with the `State`. + +In the `_MyHomePageState` class, we declared that a +`Future` todos list is expected and fetched it in the +`initState()` method - it only runs one time, which is exactly what we want. + Hurray, we just set up all the data we need! Now it's just about making it pretty :sparkles:. + +## 2 - Creating a list of todos +Let's create a new widget to encapsulate our todo list. +In Visual Studio Code, at the end of the `main.dart` file, +click `Enter` a few times and type `stful`. +The IDE will ask if we want to create a Stateful or Stateless widget. +Since we already get the information on the `HomePage` `Stateful Widget`, +we will pass it down to a `Stateless Widget` we are going to now create. + +Name your new `Stateless Widget` "`TodoList`". +Check the following code and use it. + +```dart +class TodoList extends StatelessWidget { + const TodoList({required this.todoList, super.key}); + + final List todoList; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: todoList.length, + padding: const EdgeInsets.all(16.0), + itemBuilder: (context, i) { + if (i.isOdd) return const Divider(); + final index = i ~/ 2; + + return ListTile( + title: Text( + todoList[index].title, + style: const TextStyle(fontSize: 18), + ), + ); + }, + ); + } +} +``` + +This new stateless widget receives a `todoList` as argument. +This widget will return a `ListView` widget, which has a `itemBuilder` +property that will render a list of items. + +In the `itemCount` property, we will tell how many items we want +the list to show. In this case, we want the length of the todo list. + +In the `padding` property, we will add an +[`EdgeInsets.all()`](https://api.flutter.dev/flutter/painting/EdgeInsets-class.html) +spacing. This will add a spacing of `16.0` on all directions (up, right, left, down). + +In the `itemBuilder` property, we get access to the `context` and `index` +of the rendered component. We are adding a `Divider` in between +every item. So, the `i` value *includes* the `Divider` components as well. +Therefore, to correctly fetch the index of the item in the list, +we will use the ListView index and use the +[`~/`](https://api.flutter.dev/flutter/dart-core/double/operator_truncate_divide.html) +operator. This will yield integer part of a division. +For example, `1 2 3 4 5` will be `0 1 1 2 2`. + +Now, let's use this new widget and change the `_MyHomePageState`, +more specifically the `FutureBuilder.builder` return value. + +```dart + if (snapshot.hasData) { + return TodoList(todoList: snapshot.data!); + } else if (snapshot.hasError) { + return Text('${snapshot.error}'); + } +``` + +You should now be able to scroll the list, like so! + +list diff --git a/demo_app/lib/main.dart b/demo_app/lib/main.dart index bbb7225..34902e9 100644 --- a/demo_app/lib/main.dart +++ b/demo_app/lib/main.dart @@ -77,20 +77,44 @@ class _MyHomePageState extends State { // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. child: FutureBuilder>( - future: futureTodosList, - builder: (context, snapshot) { - if (snapshot.hasData) { - return Text(snapshot.data![0].title); - } else if (snapshot.hasError) { - return Text('${snapshot.error}'); - } - - // By default, show a loading spinner. - return const CircularProgressIndicator(); - }, - ), + future: futureTodosList, + builder: (context, snapshot) { + if (snapshot.hasData) { + return TodoList(todoList: snapshot.data!); + } else if (snapshot.hasError) { + return Text('${snapshot.error}'); + } + + // By default, show a loading spinner. + return const CircularProgressIndicator(); + }, ), - ) - ; + ), + ); + } +} + +class TodoList extends StatelessWidget { + const TodoList({required this.todoList, super.key}); + + final List todoList; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: todoList.length, + padding: const EdgeInsets.all(16.0), + itemBuilder: (context, i) { + if (i.isOdd) return const Divider(); + final index = i ~/ 2; + + return ListTile( + title: Text( + todoList[index].title, + style: const TextStyle(fontSize: 18), + ), + ); + }, + ); } } From ee1f4defdffd107237351537efb2fb89e7afe251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Wed, 9 Nov 2022 14:49:10 +0000 Subject: [PATCH 19/33] feat: 3. Adding interactivity. --- README.md | 162 ++++++++++++++++++++++++++++++++++++++++- demo_app/lib/main.dart | 35 +++++++-- 2 files changed, 191 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 788f578..93b605a 100644 --- a/README.md +++ b/README.md @@ -1807,7 +1807,7 @@ Now it's just about making it pretty :sparkles:. -## 2 - Creating a list of todos +## 2. Creating a list of todos Let's create a new widget to encapsulate our todo list. In Visual Studio Code, at the end of the `main.dart` file, click `Enter` a few times and type `stful`. @@ -1879,3 +1879,163 @@ more specifically the `FutureBuilder.builder` return value. You should now be able to scroll the list, like so! list + +## 3. Adding interactivity +We want to be able to click on a todo item and +mark it as "completed". To do this, we ought to add +interactivity to our `TodoList`. +To do this, we got to convert our stateless widget +into a *stateful widget*. +Doing this is fairly simple with Visual Studio Code. +Simply double-click on `TodoList`, a yellow lightbulb +will appear to the left side. Simply click it and +click in `Convert to Stateful Widget`. + +lightbuld + +This will effectively create a new `State` to the +widget and add it. You should now have the following code: + +```dart +class TodoList extends StatefulWidget { + const TodoList({required this.todoList, super.key}); + + final List todoList; + + @override + State createState() => _TodoListState(); +} + +class _TodoListState extends State { + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: widget.todoList.length, + padding: const EdgeInsets.all(16.0), + itemBuilder: (context, i) { + if (i.isOdd) return const Divider(); + final index = i ~/ 2; + + return ListTile( + title: Text( + widget.todoList[index].title, + style: const TextStyle(fontSize: 18), + ), + ); + }, + ); + } +} +``` + +You now have the `TodoList` and `_TodoListState`, +which refers to the state of the former. Notice it +is preceded with an underscore. This enforces privacy +and is best practice for `State` objects and private fields. + +Let's change the widget to look like the following: + +```dart + +class TodoList extends StatefulWidget { + const TodoList({required this.todoList, super.key}); + + final List todoList; + + @override + State createState() => _TodoListState(); +} + +class _TodoListState extends State { + final Set _doneList = {}; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: widget.todoList.length, + padding: const EdgeInsets.all(16.0), + itemBuilder: (context, i) { + + final index = i ~/ 2; + final todoObj = widget.todoList[index]; + + if (i.isOdd) return const Divider(); + + final completed = todoObj.completed || _doneList.contains(todoObj); + + return ListTile( + title: Text( + todoObj.title, + style: TextStyle( + fontSize: 18, + decoration: completed + ? TextDecoration.lineThrough + : TextDecoration.none), + ), + onTap: (() { + setState(() { + if (completed) { + _doneList.remove(todoObj); + } else { + _doneList.add(todoObj); + } + }); + }), + ); + }, + ); + } +} +``` + +Let's break it down. The `State` object (`_TodoListState`) +now has a `_doneList` set. This set +(a set is like a list but guarantees each object is unique) +, as the underscore symbol entails, is private. +This list will hold *the list of todos marked as **done***. + +Inside the `ListView.builder()` widget, we have changed +the `itemBuilder`. We have added the following line: + +```dart +final completed = _doneList.contains(todoObj); +``` + +We are checking the item is in the `_doneList` set. +If so, we will add a strikethrough effect on the text to symbolize this. + +```dart + title: Text( + todoObj.title, + style: TextStyle( + fontSize: 18, + decoration: completed + ? TextDecoration.lineThrough + : TextDecoration.none), + ), +``` + +Now, the only thing that is left is to mark a todo item +as *complete* or *incomplete* by tapping it. +Inside the `ListTile`, we add an `onTap` property, +which is called everytime the list item is tapped, +and change the state accordingly. +If the item is completed, we mark it as incomplete, and vice-versa. + +```dart + onTap: (() { + setState(() { + if (completed) { + _doneList.remove(todoObj); + } else { + _doneList.add(todoObj); + } + }); + }), +``` + +Now, if you open your app, you can scroll and check items +and set them as `done` and reverse that action. Great job! + +![interactivity](https://user-images.githubusercontent.com/17494745/200861445-b4550a49-98cc-4f80-ba02-6ceff7fa17da.gif) + diff --git a/demo_app/lib/main.dart b/demo_app/lib/main.dart index 34902e9..1dfb289 100644 --- a/demo_app/lib/main.dart +++ b/demo_app/lib/main.dart @@ -94,25 +94,50 @@ class _MyHomePageState extends State { } } -class TodoList extends StatelessWidget { +class TodoList extends StatefulWidget { const TodoList({required this.todoList, super.key}); final List todoList; + @override + State createState() => _TodoListState(); +} + +class _TodoListState extends State { + final Set _doneList = {}; + @override Widget build(BuildContext context) { return ListView.builder( - itemCount: todoList.length, + itemCount: widget.todoList.length, padding: const EdgeInsets.all(16.0), itemBuilder: (context, i) { - if (i.isOdd) return const Divider(); + final index = i ~/ 2; + final todoObj = widget.todoList[index]; + + if (i.isOdd) return const Divider(); + + final completed = _doneList.contains(todoObj); return ListTile( title: Text( - todoList[index].title, - style: const TextStyle(fontSize: 18), + todoObj.title, + style: TextStyle( + fontSize: 18, + decoration: completed + ? TextDecoration.lineThrough + : TextDecoration.none), ), + onTap: (() { + setState(() { + if (completed) { + _doneList.remove(todoObj); + } else { + _doneList.add(todoObj); + } + }); + }), ); }, ); From 37edca945a44a901ef34dc6784cd822f50205df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Wed, 9 Nov 2022 16:06:22 +0000 Subject: [PATCH 20/33] feat: 4. Navigation. --- README.md | 189 +++++++++++++++++++++++++++++++++++++++++ demo_app/lib/main.dart | 183 ++++++++++++++++++--------------------- 2 files changed, 274 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 93b605a..38ef6cb 100644 --- a/README.md +++ b/README.md @@ -2039,3 +2039,192 @@ and set them as `done` and reverse that action. Great job! ![interactivity](https://user-images.githubusercontent.com/17494745/200861445-b4550a49-98cc-4f80-ba02-6ceff7fa17da.gif) +# 4. Adding navigation +We have added a stateful widget and are keeping track of what +todos are marked as `completed` or not. It would be great to +actually have a page where we see this list of completed items. + +Currently, our widget tree looks like this. +`MyApp` +-> `MyHomePage` (which has the `todoList` as local state) +-> `TodoList` (which has the `doneList` as local state). + +We need to merge `MyHomePage` and `TodoList` into a single +widget with having the `todoList` and `doneList` to be able to +add navigation. Mergint these two in one will lead to a new +`TodoList` widget, that will look like this. + +```dart +class TodoList extends StatefulWidget { + const TodoList({super.key}); + + @override + State createState() => _TodoListState(); +} + +class _TodoListState extends State { + late Future> futureTodosList; + final Set _doneList = {}; + + @override + void initState() { + super.initState(); + futureTodosList = TodoService().getTodos(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Todo item List'), + ), + body: FutureBuilder>( + future: futureTodosList, + builder: (context, snapshot) { + if (snapshot.hasData) { + final todolist = snapshot.data!; + + return ListView.builder( + itemCount: todolist.length, + padding: const EdgeInsets.all(16.0), + itemBuilder: (context, i) { + final index = i ~/ 2; + final todoObj = todolist[index]; + + if (i.isOdd) return const Divider(); + + final completed = _doneList.contains(todoObj); + + return ListTile( + title: Text( + todoObj.title, + style: TextStyle( + fontSize: 18, + decoration: completed + ? TextDecoration.lineThrough + : TextDecoration.none), + ), + onTap: (() { + setState(() { + if (completed) { + _doneList.remove(todoObj); + } else { + _doneList.add(todoObj); + } + }); + }), + ); + }, + ); + } else if (snapshot.hasError) { + return Text('${snapshot.error}'); + } + + // By default, show a loading spinner. + return const CircularProgressIndicator(); + }, + )); + } +} + +``` + +Nothing was fundamentally changed. +We wrapped the `TodoList` with the same widgets of +the `MyHomePage` widget. We also changed the `AppBar.title` +to `Text('Todo item list')`. + +We now also need to change the `MyApp` to call this +newly edited widget. + +```dart +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( // MODIFY with const + title: 'FlutterDemo', + home: TodoList(), // REMOVE Scaffold + ); + } +} +``` + +If you run the application, it looks the same as before. +The only difference now is that we have all the state in the same widget +(`TodoList`). + +Inside the `_TodoListState` widget state class, let's add a button in the app bar +to navigate to the new page. + +```dart + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Todo item List'), + actions: [ + IconButton( + icon: const Icon(Icons.list), + onPressed: _pushCompleted, + tooltip: 'Completed todo list', + ), + ], + ), +``` + +Let's implement the `_pushCompleted` function, that is executed +everytime the icon button is clicked on the appbar. +We want to navigate to the page that shows the completed todo items. +Add the following function in `_TodoListState`. + +```dart + void _pushCompleted() { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) { + final tiles = _doneList.map( + (todo) { + return ListTile( + title: Text( + todo.title, + style: const TextStyle(fontSize: 18), + ), + ); + }, + ); + final divided = tiles.isNotEmpty + ? ListTile.divideTiles( + context: context, + tiles: tiles, + ).toList() + : []; + + return Scaffold( + appBar: AppBar( + title: const Text('Completed todo list'), + ), + body: ListView(children: divided), + ); + }, + ), + ); + } +``` + +Let's break this down. We use the `Navigator` to push a new screen to +the stack. We pass the widget's `context` and then use the `push()` function +to add the screen to the stack. +In this case, we are pushing a `MaterialPageRoute`, inside the `builder` +property, we return a `Scaffold` object with an `appBar` and a `body`. +Inside this `body`, we are rendering a `ListView` with each todo item +inside the `_doneList` set. + +Since we are using `MaterialPageRoute` and `Scaffold`, the back button is automatically added +to the appbar, making it possible to *pop* the screen and go back to the +screen showing the todo list. + +If we rerun our app, we can now navigate between pages. Hurray! :tada: + +![navigation](https://user-images.githubusercontent.com/17494745/200880357-314bb388-5c0c-4955-ac22-f9ec59e418a6.gif) \ No newline at end of file diff --git a/demo_app/lib/main.dart b/demo_app/lib/main.dart index 1dfb289..1f94f5d 100644 --- a/demo_app/lib/main.dart +++ b/demo_app/lib/main.dart @@ -13,45 +13,55 @@ class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { - return MaterialApp( + return const MaterialApp( title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. - primarySwatch: Colors.blue, - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), + home: TodoList(), ); } } -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; +class TodoList extends StatefulWidget { + const TodoList({super.key}); @override - State createState() => _MyHomePageState(); + State createState() => _TodoListState(); } -class _MyHomePageState extends State { +class _TodoListState extends State { late Future> futureTodosList; + final Set _doneList = {}; + + void _pushCompleted() { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) { + final tiles = _doneList.map( + (todo) { + return ListTile( + title: Text( + todo.title, + style: const TextStyle(fontSize: 18), + ), + ); + }, + ); + final divided = tiles.isNotEmpty + ? ListTile.divideTiles( + context: context, + tiles: tiles, + ).toList() + : []; + + return Scaffold( + appBar: AppBar( + title: const Text('Completed todo list'), + ), + body: ListView(children: divided), + ); + }, + ), + ); + } @override void initState() { @@ -61,26 +71,55 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. return Scaffold( - appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: FutureBuilder>( + appBar: AppBar( + title: const Text('Todo item List'), + actions: [ + IconButton( + icon: const Icon(Icons.list), + onPressed: _pushCompleted, + tooltip: 'Completed todo list', + ), + ], + ), + body: FutureBuilder>( future: futureTodosList, builder: (context, snapshot) { if (snapshot.hasData) { - return TodoList(todoList: snapshot.data!); + final todolist = snapshot.data!; + + return ListView.builder( + itemCount: todolist.length, + padding: const EdgeInsets.all(16.0), + itemBuilder: (context, i) { + final index = i ~/ 2; + final todoObj = todolist[index]; + + if (i.isOdd) return const Divider(); + + final completed = _doneList.contains(todoObj); + + return ListTile( + title: Text( + todoObj.title, + style: TextStyle( + fontSize: 18, + decoration: completed + ? TextDecoration.lineThrough + : TextDecoration.none), + ), + onTap: (() { + setState(() { + if (completed) { + _doneList.remove(todoObj); + } else { + _doneList.add(todoObj); + } + }); + }), + ); + }, + ); } else if (snapshot.hasError) { return Text('${snapshot.error}'); } @@ -88,58 +127,6 @@ class _MyHomePageState extends State { // By default, show a loading spinner. return const CircularProgressIndicator(); }, - ), - ), - ); - } -} - -class TodoList extends StatefulWidget { - const TodoList({required this.todoList, super.key}); - - final List todoList; - - @override - State createState() => _TodoListState(); -} - -class _TodoListState extends State { - final Set _doneList = {}; - - @override - Widget build(BuildContext context) { - return ListView.builder( - itemCount: widget.todoList.length, - padding: const EdgeInsets.all(16.0), - itemBuilder: (context, i) { - - final index = i ~/ 2; - final todoObj = widget.todoList[index]; - - if (i.isOdd) return const Divider(); - - final completed = _doneList.contains(todoObj); - - return ListTile( - title: Text( - todoObj.title, - style: TextStyle( - fontSize: 18, - decoration: completed - ? TextDecoration.lineThrough - : TextDecoration.none), - ), - onTap: (() { - setState(() { - if (completed) { - _doneList.remove(todoObj); - } else { - _doneList.add(todoObj); - } - }); - }), - ); - }, - ); + )); } } From 3864fa79de192b524f2bdf959c9b42fbd7cc6488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Wed, 9 Nov 2022 16:12:33 +0000 Subject: [PATCH 21/33] feat: 5. Finishing touches. --- README.md | 27 ++++++++++++++++++++++++++- demo_app/lib/main.dart | 13 +++++++++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 38ef6cb..12e1010 100644 --- a/README.md +++ b/README.md @@ -2227,4 +2227,29 @@ screen showing the todo list. If we rerun our app, we can now navigate between pages. Hurray! :tada: -![navigation](https://user-images.githubusercontent.com/17494745/200880357-314bb388-5c0c-4955-ac22-f9ec59e418a6.gif) \ No newline at end of file +![navigation](https://user-images.githubusercontent.com/17494745/200880357-314bb388-5c0c-4955-ac22-f9ec59e418a6.gif) + +# 5. Finishing touches +We can quickly custmize the theme of the app, and it's title. +Let's change the colors and give our fancy app a new title. + +```dart + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Todo App', + theme: ThemeData( + appBarTheme: const AppBarTheme( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + ) + ), + home: const TodoList(), + ); + } +``` + +Your app should look like this, now! +You can choose the colors you like. Go creative! :tada: + +final diff --git a/demo_app/lib/main.dart b/demo_app/lib/main.dart index 1f94f5d..df27fdd 100644 --- a/demo_app/lib/main.dart +++ b/demo_app/lib/main.dart @@ -10,12 +10,17 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { - return const MaterialApp( - title: 'Flutter Demo', - home: TodoList(), + return MaterialApp( + title: 'Todo App', + theme: ThemeData( + appBarTheme: const AppBarTheme( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + ) + ), + home: const TodoList(), ); } } From ebe2054d4ffb878998ad49dd0f373184e4bacf0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Wed, 9 Nov 2022 16:35:07 +0000 Subject: [PATCH 22/33] feat: Deleting unused TDD app. Updating README. --- README.md | 16 + tdd_architeture/.gitignore | 37 -- tdd_architeture/.metadata | 10 - tdd_architeture/README.md | 16 - tdd_architeture/android/.gitignore | 7 - tdd_architeture/android/app/build.gradle | 61 --- .../android/app/src/debug/AndroidManifest.xml | 7 - .../android/app/src/main/AndroidManifest.xml | 30 - .../clean_architeture_tdd/MainActivity.java | 13 - .../main/res/drawable/launch_background.xml | 12 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 0 bytes .../app/src/main/res/values/styles.xml | 8 - .../app/src/profile/AndroidManifest.xml | 7 - tdd_architeture/android/build.gradle | 29 - tdd_architeture/android/gradle.properties | 4 - .../gradle/wrapper/gradle-wrapper.properties | 6 - tdd_architeture/android/settings.gradle | 15 - .../clean_architeture_tdd/.gitignore | 37 -- .../clean_architeture_tdd/.metadata | 10 - tdd_architeture/ios/.gitignore | 32 -- .../ios/Flutter/AppFrameworkInfo.plist | 26 - tdd_architeture/ios/Flutter/Debug.xcconfig | 2 - tdd_architeture/ios/Flutter/Release.xcconfig | 2 - tdd_architeture/ios/Podfile | 87 --- .../ios/Runner.xcodeproj/project.pbxproj | 511 ------------------ .../contents.xcworkspacedata | 7 - .../xcshareddata/xcschemes/Runner.xcscheme | 91 ---- .../contents.xcworkspacedata | 7 - tdd_architeture/ios/Runner/AppDelegate.h | 6 - tdd_architeture/ios/Runner/AppDelegate.m | 13 - .../AppIcon.appiconset/Contents.json | 122 ----- .../Icon-App-1024x1024@1x.png | Bin 10932 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 564 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1588 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 1025 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1716 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1920 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 1895 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 3831 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1888 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 3294 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 3612 -> 0 bytes .../LaunchImage.imageset/Contents.json | 23 - .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/README.md | 5 - .../Runner/Base.lproj/LaunchScreen.storyboard | 37 -- .../ios/Runner/Base.lproj/Main.storyboard | 26 - tdd_architeture/ios/Runner/Info.plist | 45 -- tdd_architeture/ios/Runner/main.m | 9 - .../lib/core/error/exceptions.dart | 8 - tdd_architeture/lib/core/error/failures.dart | 11 - .../lib/core/network/network_info.dart | 14 - .../lib/core/platform/network_info.dart | 14 - .../lib/core/usecases/usecase.dart | 13 - .../lib/core/util/input_converter.dart | 17 - .../number_trivia_local_data_source.dart | 42 -- .../number_trivia_remote_data_source.dart | 48 -- .../data/models/number_trivia_model.dart | 23 - .../number_trivia_repository_impl.dart | 61 --- .../domain/entities/number_trivia.dart | 15 - .../domain/number_trivia_repository.dart | 8 - .../usecases/get_concrete_number_trivia.dart | 30 - .../usecases/get_random_number_trivia.dart | 18 - .../number_trivia/presentation/bloc/bloc.dart | 3 - .../presentation/bloc/number_trivia_bloc.dart | 83 --- .../bloc/number_trivia_event.dart | 19 - .../bloc/number_trivia_state.dart | 31 -- .../pages/number_trivia_page.dart | 58 -- .../presentation/widgets/loading_widget.dart | 17 - .../presentation/widgets/message_display.dart | 26 - .../presentation/widgets/trivia_controls.dart | 70 --- .../presentation/widgets/trivia_display.dart | 37 -- .../presentation/widgets/widgets.dart | 4 - tdd_architeture/lib/injection_container.dart | 60 -- tdd_architeture/lib/main.dart | 23 - tdd_architeture/pubspec.yaml | 76 --- .../test/core/network/network_info_test.dart | 34 -- .../test/core/util/input_converter_test.dart | 49 -- .../number_trivia_local_data_source_test.dart | 67 --- ...number_trivia_remote_data_source_test.dart | 128 ----- .../Data/models/number_trivia_model_test.dart | 36 -- .../number_trivia_repository_impl_test.dart | 247 --------- .../get_concrete_number_trivia_test.dart | 38 -- .../get_random_number_trivia_test.dart | 38 -- .../bloc/number_trivia_bloc_test.dart | 225 -------- .../test/fixtures/Fixture_reader.dart | 3 - tdd_architeture/test/fixtures/trivia.json | 6 - .../test/fixtures/trivia_cached.json | 4 - .../test/fixtures/trivia_double.json | 6 - 99 files changed, 16 insertions(+), 3070 deletions(-) delete mode 100644 tdd_architeture/.gitignore delete mode 100644 tdd_architeture/.metadata delete mode 100644 tdd_architeture/README.md delete mode 100644 tdd_architeture/android/.gitignore delete mode 100644 tdd_architeture/android/app/build.gradle delete mode 100644 tdd_architeture/android/app/src/debug/AndroidManifest.xml delete mode 100644 tdd_architeture/android/app/src/main/AndroidManifest.xml delete mode 100644 tdd_architeture/android/app/src/main/java/com/martins/clean_architeture_tdd/MainActivity.java delete mode 100644 tdd_architeture/android/app/src/main/res/drawable/launch_background.xml delete mode 100644 tdd_architeture/android/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 tdd_architeture/android/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 tdd_architeture/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 tdd_architeture/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 tdd_architeture/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 tdd_architeture/android/app/src/main/res/values/styles.xml delete mode 100644 tdd_architeture/android/app/src/profile/AndroidManifest.xml delete mode 100644 tdd_architeture/android/build.gradle delete mode 100644 tdd_architeture/android/gradle.properties delete mode 100644 tdd_architeture/android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 tdd_architeture/android/settings.gradle delete mode 100644 tdd_architeture/clean_architeture_tdd/.gitignore delete mode 100644 tdd_architeture/clean_architeture_tdd/.metadata delete mode 100644 tdd_architeture/ios/.gitignore delete mode 100644 tdd_architeture/ios/Flutter/AppFrameworkInfo.plist delete mode 100644 tdd_architeture/ios/Flutter/Debug.xcconfig delete mode 100644 tdd_architeture/ios/Flutter/Release.xcconfig delete mode 100644 tdd_architeture/ios/Podfile delete mode 100644 tdd_architeture/ios/Runner.xcodeproj/project.pbxproj delete mode 100644 tdd_architeture/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 tdd_architeture/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 tdd_architeture/ios/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 tdd_architeture/ios/Runner/AppDelegate.h delete mode 100644 tdd_architeture/ios/Runner/AppDelegate.m delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png delete mode 100644 tdd_architeture/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md delete mode 100644 tdd_architeture/ios/Runner/Base.lproj/LaunchScreen.storyboard delete mode 100644 tdd_architeture/ios/Runner/Base.lproj/Main.storyboard delete mode 100644 tdd_architeture/ios/Runner/Info.plist delete mode 100644 tdd_architeture/ios/Runner/main.m delete mode 100644 tdd_architeture/lib/core/error/exceptions.dart delete mode 100644 tdd_architeture/lib/core/error/failures.dart delete mode 100644 tdd_architeture/lib/core/network/network_info.dart delete mode 100644 tdd_architeture/lib/core/platform/network_info.dart delete mode 100644 tdd_architeture/lib/core/usecases/usecase.dart delete mode 100644 tdd_architeture/lib/core/util/input_converter.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/data/datasources/number_trivia_local_data_source.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/data/models/number_trivia_model.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/data/repositories/number_trivia_repository_impl.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/domain/entities/number_trivia.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/domain/number_trivia_repository.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/domain/usecases/get_random_number_trivia.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/presentation/bloc/bloc.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/presentation/bloc/number_trivia_bloc.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/presentation/bloc/number_trivia_event.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/presentation/bloc/number_trivia_state.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/presentation/pages/number_trivia_page.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/presentation/widgets/loading_widget.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/presentation/widgets/message_display.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/presentation/widgets/trivia_controls.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/presentation/widgets/trivia_display.dart delete mode 100644 tdd_architeture/lib/features/number_trivia/presentation/widgets/widgets.dart delete mode 100644 tdd_architeture/lib/injection_container.dart delete mode 100644 tdd_architeture/lib/main.dart delete mode 100644 tdd_architeture/pubspec.yaml delete mode 100644 tdd_architeture/test/core/network/network_info_test.dart delete mode 100644 tdd_architeture/test/core/util/input_converter_test.dart delete mode 100644 tdd_architeture/test/features/number_trivia/Data/datasources/number_trivia_local_data_source_test.dart delete mode 100644 tdd_architeture/test/features/number_trivia/Data/datasources/number_trivia_remote_data_source_test.dart delete mode 100644 tdd_architeture/test/features/number_trivia/Data/models/number_trivia_model_test.dart delete mode 100644 tdd_architeture/test/features/number_trivia/Data/repositories/number_trivia_repository_impl_test.dart delete mode 100644 tdd_architeture/test/features/number_trivia/Domain/usescases/get_concrete_number_trivia_test.dart delete mode 100644 tdd_architeture/test/features/number_trivia/Domain/usescases/get_random_number_trivia_test.dart delete mode 100644 tdd_architeture/test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart delete mode 100644 tdd_architeture/test/fixtures/Fixture_reader.dart delete mode 100644 tdd_architeture/test/fixtures/trivia.json delete mode 100644 tdd_architeture/test/fixtures/trivia_cached.json delete mode 100644 tdd_architeture/test/fixtures/trivia_double.json diff --git a/README.md b/README.md index 12e1010..c5f24a9 100644 --- a/README.md +++ b/README.md @@ -2253,3 +2253,19 @@ Your app should look like this, now! You can choose the colors you like. Go creative! :tada: final + + +# Final remarks πŸ‘‹ +In this document (if you actually read it all the way through πŸ˜‰), +you went from 0 to hero with Flutter. You learnt important +principles and you *applied* them to create your own app in just +around 20 minutes! Give yourself a pat on the back! :tada: + +If you wish to learn a bit more, take a look +at this repo's `guides` folder to +learn about [logging in with Firebase](./guides/login-firebase-tutorial.md) +or [webviews in Flutter](./guides/webview-tutorial.md) + +If you want to see more examples, check these out! +- [flutter-todo-list-tutorial](https://github.com/dwyl/flutter-todo-list-tutorial) +- [flutter-counter-example](https://github.com/dwyl/flutter-counter-example) \ No newline at end of file diff --git a/tdd_architeture/.gitignore b/tdd_architeture/.gitignore deleted file mode 100644 index ae1f183..0000000 --- a/tdd_architeture/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ - -# Web related -lib/generated_plugin_registrant.dart - -# Exceptions to above rules. -!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/tdd_architeture/.metadata b/tdd_architeture/.metadata deleted file mode 100644 index 5d1241e..0000000 --- a/tdd_architeture/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 9f5ff2306bb3e30b2b98eee79cd231b1336f41f4 - channel: stable - -project_type: app diff --git a/tdd_architeture/README.md b/tdd_architeture/README.md deleted file mode 100644 index cebf16a..0000000 --- a/tdd_architeture/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# clean_architeture_tdd - -A new Flutter application. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/tdd_architeture/android/.gitignore b/tdd_architeture/android/.gitignore deleted file mode 100644 index bc2100d..0000000 --- a/tdd_architeture/android/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java diff --git a/tdd_architeture/android/app/build.gradle b/tdd_architeture/android/app/build.gradle deleted file mode 100644 index ee0832c..0000000 --- a/tdd_architeture/android/app/build.gradle +++ /dev/null @@ -1,61 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 28 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.martins.clean_architeture_tdd" - minSdkVersion 16 - targetSdkVersion 28 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/tdd_architeture/android/app/src/debug/AndroidManifest.xml b/tdd_architeture/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 476c23f..0000000 --- a/tdd_architeture/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/tdd_architeture/android/app/src/main/AndroidManifest.xml b/tdd_architeture/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 8595530..0000000 --- a/tdd_architeture/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - diff --git a/tdd_architeture/android/app/src/main/java/com/martins/clean_architeture_tdd/MainActivity.java b/tdd_architeture/android/app/src/main/java/com/martins/clean_architeture_tdd/MainActivity.java deleted file mode 100644 index ec69222..0000000 --- a/tdd_architeture/android/app/src/main/java/com/martins/clean_architeture_tdd/MainActivity.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.martins.clean_architeture_tdd; - -import androidx.annotation.NonNull; -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class MainActivity extends FlutterActivity { - @Override - public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { - GeneratedPluginRegistrant.registerWith(flutterEngine); - } -} diff --git a/tdd_architeture/android/app/src/main/res/drawable/launch_background.xml b/tdd_architeture/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f..0000000 --- a/tdd_architeture/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/tdd_architeture/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/tdd_architeture/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/tdd_architeture/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/tdd_architeture/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/tdd_architeture/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/tdd_architeture/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/tdd_architeture/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/tdd_architeture/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/tdd_architeture/android/app/src/main/res/values/styles.xml b/tdd_architeture/android/app/src/main/res/values/styles.xml deleted file mode 100644 index 00fa441..0000000 --- a/tdd_architeture/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/tdd_architeture/android/app/src/profile/AndroidManifest.xml b/tdd_architeture/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 476c23f..0000000 --- a/tdd_architeture/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/tdd_architeture/android/build.gradle b/tdd_architeture/android/build.gradle deleted file mode 100644 index e0d7ae2..0000000 --- a/tdd_architeture/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -allprojects { - repositories { - google() - jcenter() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/tdd_architeture/android/gradle.properties b/tdd_architeture/android/gradle.properties deleted file mode 100644 index 38c8d45..0000000 --- a/tdd_architeture/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/tdd_architeture/android/gradle/wrapper/gradle-wrapper.properties b/tdd_architeture/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 296b146..0000000 --- a/tdd_architeture/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Fri Jun 23 08:50:38 CEST 2017 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/tdd_architeture/android/settings.gradle b/tdd_architeture/android/settings.gradle deleted file mode 100644 index 5a2f14f..0000000 --- a/tdd_architeture/android/settings.gradle +++ /dev/null @@ -1,15 +0,0 @@ -include ':app' - -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() - -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } -} - -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} diff --git a/tdd_architeture/clean_architeture_tdd/.gitignore b/tdd_architeture/clean_architeture_tdd/.gitignore deleted file mode 100644 index ae1f183..0000000 --- a/tdd_architeture/clean_architeture_tdd/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ - -# Web related -lib/generated_plugin_registrant.dart - -# Exceptions to above rules. -!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/tdd_architeture/clean_architeture_tdd/.metadata b/tdd_architeture/clean_architeture_tdd/.metadata deleted file mode 100644 index 5d1241e..0000000 --- a/tdd_architeture/clean_architeture_tdd/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 9f5ff2306bb3e30b2b98eee79cd231b1336f41f4 - channel: stable - -project_type: app diff --git a/tdd_architeture/ios/.gitignore b/tdd_architeture/ios/.gitignore deleted file mode 100644 index e96ef60..0000000 --- a/tdd_architeture/ios/.gitignore +++ /dev/null @@ -1,32 +0,0 @@ -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/tdd_architeture/ios/Flutter/AppFrameworkInfo.plist b/tdd_architeture/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6b4c0f7..0000000 --- a/tdd_architeture/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 8.0 - - diff --git a/tdd_architeture/ios/Flutter/Debug.xcconfig b/tdd_architeture/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba1..0000000 --- a/tdd_architeture/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/tdd_architeture/ios/Flutter/Release.xcconfig b/tdd_architeture/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e934..0000000 --- a/tdd_architeture/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/tdd_architeture/ios/Podfile b/tdd_architeture/ios/Podfile deleted file mode 100644 index 98a90b8..0000000 --- a/tdd_architeture/ios/Podfile +++ /dev/null @@ -1,87 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '9.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def parse_KV_file(file, separator='=') - file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path - return []; - end - generated_key_values = {} - skip_line_start_symbols = ["#", "/"] - File.foreach(file_abs_path) do |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - podpath = File.expand_path("#{path}", file_abs_path) - generated_key_values[podname] = podpath - else - puts "Invalid plugin specification: #{line}" - end - end - generated_key_values -end - -target 'Runner' do - # Flutter Pod - - copied_flutter_dir = File.join(__dir__, 'Flutter') - copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') - copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') - unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) - # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. - # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. - # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. - - generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') - unless File.exist?(generated_xcode_build_settings_path) - raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) - cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; - - unless File.exist?(copied_framework_path) - FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) - end - unless File.exist?(copied_podspec_path) - FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) - end - end - - # Keep pod path relative so it can be checked into Podfile.lock. - pod 'Flutter', :path => 'Flutter' - - # Plugin Pods - - # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock - # referring to absolute paths on developers' machines. - system('rm -rf .symlinks') - system('mkdir -p .symlinks/plugins') - plugin_pods = parse_KV_file('../.flutter-plugins') - plugin_pods.each do |name, path| - symlink = File.join('.symlinks', 'plugins', name) - File.symlink(path, symlink) - pod name, :path => File.join(symlink, 'ios') - end -end - -# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. -install! 'cocoapods', :disable_input_output_paths => true - -post_install do |installer| - installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings['ENABLE_BITCODE'] = 'NO' - end - end -end diff --git a/tdd_architeture/ios/Runner.xcodeproj/project.pbxproj b/tdd_architeture/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index a1306f3..0000000 --- a/tdd_architeture/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,511 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1020; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.martins.cleanArchitetureTdd; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.martins.cleanArchitetureTdd; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.martins.cleanArchitetureTdd; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/tdd_architeture/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/tdd_architeture/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a1..0000000 --- a/tdd_architeture/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/tdd_architeture/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/tdd_architeture/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index a28140c..0000000 --- a/tdd_architeture/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tdd_architeture/ios/Runner.xcworkspace/contents.xcworkspacedata b/tdd_architeture/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a1..0000000 --- a/tdd_architeture/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/tdd_architeture/ios/Runner/AppDelegate.h b/tdd_architeture/ios/Runner/AppDelegate.h deleted file mode 100644 index 36e21bb..0000000 --- a/tdd_architeture/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/tdd_architeture/ios/Runner/AppDelegate.m b/tdd_architeture/ios/Runner/AppDelegate.m deleted file mode 100644 index 70e8393..0000000 --- a/tdd_architeture/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,13 +0,0 @@ -#import "AppDelegate.h" -#import "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fa..0000000 --- a/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4725e9b0ddb1deab583e5b5102493aa332..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10932 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 diff --git a/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca859a3f474b03065bef75ba58a9e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ diff --git a/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb86cdfe0d15b4b0d98334a86163658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1716 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 diff --git a/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee1c98386d13b17e89f719e83555b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 diff --git a/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3831 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 diff --git a/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/tdd_architeture/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1888 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 iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/tdd_architeture/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/tdd_architeture/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/tdd_architeture/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/tdd_architeture/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/tdd_architeture/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/tdd_architeture/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725..0000000 --- a/tdd_architeture/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/tdd_architeture/ios/Runner/Base.lproj/LaunchScreen.storyboard b/tdd_architeture/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c..0000000 --- a/tdd_architeture/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tdd_architeture/ios/Runner/Base.lproj/Main.storyboard b/tdd_architeture/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c2851..0000000 --- a/tdd_architeture/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tdd_architeture/ios/Runner/Info.plist b/tdd_architeture/ios/Runner/Info.plist deleted file mode 100644 index 4401fca..0000000 --- a/tdd_architeture/ios/Runner/Info.plist +++ /dev/null @@ -1,45 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - clean_architeture_tdd - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/tdd_architeture/ios/Runner/main.m b/tdd_architeture/ios/Runner/main.m deleted file mode 100644 index dff6597..0000000 --- a/tdd_architeture/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/tdd_architeture/lib/core/error/exceptions.dart b/tdd_architeture/lib/core/error/exceptions.dart deleted file mode 100644 index e2a9463..0000000 --- a/tdd_architeture/lib/core/error/exceptions.dart +++ /dev/null @@ -1,8 +0,0 @@ -class ServerException implements Exception{ - - -} -class CacheException implements Exception{ - - -} \ No newline at end of file diff --git a/tdd_architeture/lib/core/error/failures.dart b/tdd_architeture/lib/core/error/failures.dart deleted file mode 100644 index 480793d..0000000 --- a/tdd_architeture/lib/core/error/failures.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:equatable/equatable.dart'; - -abstract class Failure extends Equatable { - @override - List get props => []; -} - -// General failures -class ServerFailure extends Failure {} - -class CacheFailure extends Failure {} \ No newline at end of file diff --git a/tdd_architeture/lib/core/network/network_info.dart b/tdd_architeture/lib/core/network/network_info.dart deleted file mode 100644 index ecc84e5..0000000 --- a/tdd_architeture/lib/core/network/network_info.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:data_connection_checker/data_connection_checker.dart'; - -abstract class NetworkInfo { - Future get isConnected; -} - -class NetworkInfoImpl implements NetworkInfo { - final DataConnectionChecker connectionChecker; - - NetworkInfoImpl(this.connectionChecker); - - @override - Future get isConnected => connectionChecker.hasConnection; -} \ No newline at end of file diff --git a/tdd_architeture/lib/core/platform/network_info.dart b/tdd_architeture/lib/core/platform/network_info.dart deleted file mode 100644 index ecc84e5..0000000 --- a/tdd_architeture/lib/core/platform/network_info.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:data_connection_checker/data_connection_checker.dart'; - -abstract class NetworkInfo { - Future get isConnected; -} - -class NetworkInfoImpl implements NetworkInfo { - final DataConnectionChecker connectionChecker; - - NetworkInfoImpl(this.connectionChecker); - - @override - Future get isConnected => connectionChecker.hasConnection; -} \ No newline at end of file diff --git a/tdd_architeture/lib/core/usecases/usecase.dart b/tdd_architeture/lib/core/usecases/usecase.dart deleted file mode 100644 index e09642b..0000000 --- a/tdd_architeture/lib/core/usecases/usecase.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:dartz/dartz.dart'; -import 'package:equatable/equatable.dart'; - -import '../error/failures.dart'; - -abstract class UseCase { - Future> call(Params params); -} - -class NoParams extends Equatable { - @override - List get props => []; -} \ No newline at end of file diff --git a/tdd_architeture/lib/core/util/input_converter.dart b/tdd_architeture/lib/core/util/input_converter.dart deleted file mode 100644 index aba4f1f..0000000 --- a/tdd_architeture/lib/core/util/input_converter.dart +++ /dev/null @@ -1,17 +0,0 @@ - -import 'package:clean_architeture_tdd/core/error/failures.dart'; -import 'package:dartz/dartz.dart'; - -class InputConverter { - Either stringToUnsignedInteger(String str) { - try { - final integer = int.parse(str); - if (integer < 0) throw FormatException(); - return Right(integer); - } on FormatException { - return Left(InvalidInputFailure()); - } - } -} - -class InvalidInputFailure extends Failure {} \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/data/datasources/number_trivia_local_data_source.dart b/tdd_architeture/lib/features/number_trivia/data/datasources/number_trivia_local_data_source.dart deleted file mode 100644 index 20dbe2d..0000000 --- a/tdd_architeture/lib/features/number_trivia/data/datasources/number_trivia_local_data_source.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'dart:convert'; -import 'package:clean_architeture_tdd/core/error/exceptions.dart'; -import 'package:meta/meta.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../models/number_trivia_model.dart'; - -abstract class NumberTriviaLocalDataSource { - /// Gets the cached [NumberTriviaModel] which was gotten the last time - /// the user had an internet connection. - /// - /// Throws [CacheException] if no cached data is present. - Future getLastNumberTrivia(); - - Future cacheNumberTrivia(NumberTriviaModel triviaToCache); -} - -const CACHED_NUMBER_TRIVIA = 'CACHED_NUMBER_TRIVIA'; - -class NumberTriviaLocalDataSourceImpl implements NumberTriviaLocalDataSource { - final SharedPreferences sharedPreferences; - - NumberTriviaLocalDataSourceImpl({@required this.sharedPreferences}); - - @override - Future getLastNumberTrivia() { - final jsonString = sharedPreferences.getString(CACHED_NUMBER_TRIVIA); - if (jsonString != null) { - return Future.value(NumberTriviaModel.fromJson(json.decode(jsonString))); - } else { - throw CacheException(); - } - } - - @override - Future cacheNumberTrivia(NumberTriviaModel triviaToCache) { - return sharedPreferences.setString( - CACHED_NUMBER_TRIVIA, - json.encode(triviaToCache.toJson()), - ); - } -} \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart b/tdd_architeture/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart deleted file mode 100644 index c29f354..0000000 --- a/tdd_architeture/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'dart:convert'; - -import 'package:http/http.dart' as http; -import 'package:meta/meta.dart'; - -import '../../../../core/error/exceptions.dart'; -import '../models/number_trivia_model.dart'; - -abstract class NumberTriviaRemoteDataSource { - /// Calls the http://numbersapi.com/{number} endpoint. - /// - /// Throws a [ServerException] for all error codes. - Future getConcreteNumberTrivia(int number); - - /// Calls the http://numbersapi.com/random endpoint. - /// - /// Throws a [ServerException] for all error codes. - Future getRandomNumberTrivia(); -} - -class NumberTriviaRemoteDataSourceImpl implements NumberTriviaRemoteDataSource { - final http.Client client; - - NumberTriviaRemoteDataSourceImpl({@required this.client}); - - @override - Future getConcreteNumberTrivia(int number) => - _getTriviaFromUrl('http://numbersapi.com/$number'); - - @override - Future getRandomNumberTrivia() => - _getTriviaFromUrl('http://numbersapi.com/random'); - - Future _getTriviaFromUrl(String url) async { - final response = await client.get( - url, - headers: { - 'Content-Type': 'application/json', - }, - ); - - if (response.statusCode == 200) { - return NumberTriviaModel.fromJson(json.decode(response.body)); - } else { - throw ServerException(); - } - } -} \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/data/models/number_trivia_model.dart b/tdd_architeture/lib/features/number_trivia/data/models/number_trivia_model.dart deleted file mode 100644 index 7b47ee3..0000000 --- a/tdd_architeture/lib/features/number_trivia/data/models/number_trivia_model.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:clean_architeture_tdd/features/number_trivia/domain/entities/number_trivia.dart'; -import 'package:meta/meta.dart'; - -class NumberTriviaModel extends NumberTrivia { - NumberTriviaModel({ - @required String text, - @required int number, - }) : super(text: text, number: number); - - factory NumberTriviaModel.fromJson(Map json) { - return NumberTriviaModel( - text: json['text'], - number: (json['number'] as num).toInt(), - ); - } - - Map toJson() { - return { - 'text': text, - 'number': number, - }; - } -} \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/data/repositories/number_trivia_repository_impl.dart b/tdd_architeture/lib/features/number_trivia/data/repositories/number_trivia_repository_impl.dart deleted file mode 100644 index 596062c..0000000 --- a/tdd_architeture/lib/features/number_trivia/data/repositories/number_trivia_repository_impl.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:clean_architeture_tdd/features/number_trivia/domain/number_trivia_repository.dart'; -import 'package:dartz/dartz.dart'; -import 'package:meta/meta.dart'; - -import '../../../../core/error/failures.dart'; -import '../../../../core/error/exceptions.dart'; -import '../../../../core/network/network_info.dart'; -import '../../domain/entities/number_trivia.dart'; -import '../datasources/number_trivia_local_data_source.dart'; -import '../datasources/number_trivia_remote_data_source.dart'; - -typedef Future _ConcreteOrRandomChooser(); - -class NumberTriviaRepositoryImpl implements NumberTriviaRepository { - final NumberTriviaRemoteDataSource remoteDataSource; - final NumberTriviaLocalDataSource localDataSource; - final NetworkInfo networkInfo; - - NumberTriviaRepositoryImpl({ - @required this.remoteDataSource, - @required this.localDataSource, - @required this.networkInfo, - }); - - @override - Future> getConcreteNumberTrivia( - int number, - ) async { - return await _getTrivia(() { - return remoteDataSource.getConcreteNumberTrivia(number); - }); - } - - @override - Future> getRandomNumberTrivia() async { - return await _getTrivia(() { - return remoteDataSource.getRandomNumberTrivia(); - }); - } - - Future> _getTrivia( - _ConcreteOrRandomChooser getConcreteOrRandom, - ) async { - if (await networkInfo.isConnected) { - try { - final remoteTrivia = await getConcreteOrRandom(); - localDataSource.cacheNumberTrivia(remoteTrivia); - return Right(remoteTrivia); - } on ServerException { - return Left(ServerFailure()); - } - } else { - try { - final localTrivia = await localDataSource.getLastNumberTrivia(); - return Right(localTrivia); - } on CacheException { - return Left(CacheFailure()); - } - } - } -} \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/domain/entities/number_trivia.dart b/tdd_architeture/lib/features/number_trivia/domain/entities/number_trivia.dart deleted file mode 100644 index 8e7e0ee..0000000 --- a/tdd_architeture/lib/features/number_trivia/domain/entities/number_trivia.dart +++ /dev/null @@ -1,15 +0,0 @@ -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, - }); - - @override - List get props => [text, number]; -} diff --git a/tdd_architeture/lib/features/number_trivia/domain/number_trivia_repository.dart b/tdd_architeture/lib/features/number_trivia/domain/number_trivia_repository.dart deleted file mode 100644 index 0e9bcc6..0000000 --- a/tdd_architeture/lib/features/number_trivia/domain/number_trivia_repository.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:clean_architeture_tdd/core/error/failures.dart'; -import 'package:clean_architeture_tdd/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/tdd_architeture/lib/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart b/tdd_architeture/lib/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart deleted file mode 100644 index a65e070..0000000 --- a/tdd_architeture/lib/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart +++ /dev/null @@ -1,30 +0,0 @@ - -import 'package:clean_architeture_tdd/features/number_trivia/domain/number_trivia_repository.dart'; -import 'package:dartz/dartz.dart'; -import 'package:equatable/equatable.dart'; -import 'package:meta/meta.dart'; - -import '../../../../core/error/failures.dart'; -import '../../../../core/usecases/usecase.dart'; -import '../entities/number_trivia.dart'; - - -class GetConcreteNumberTrivia implements UseCase { - final NumberTriviaRepository repository; - - GetConcreteNumberTrivia(this.repository); - - @override - Future> call(Params params) async { - return await repository.getConcreteNumberTrivia(params.number); - } -} - -class Params extends Equatable { - final int number; - - Params({@required this.number}); - - @override - List get props => [number]; -} diff --git a/tdd_architeture/lib/features/number_trivia/domain/usecases/get_random_number_trivia.dart b/tdd_architeture/lib/features/number_trivia/domain/usecases/get_random_number_trivia.dart deleted file mode 100644 index 4acf828..0000000 --- a/tdd_architeture/lib/features/number_trivia/domain/usecases/get_random_number_trivia.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:dartz/dartz.dart'; - -import '../../../../core/error/failures.dart'; -import '../../../../core/usecases/usecase.dart'; -import '../entities/number_trivia.dart'; -import '../number_trivia_repository.dart'; - - -class GetRandomNumberTrivia implements UseCase { - final NumberTriviaRepository repository; - - GetRandomNumberTrivia(this.repository); - - @override - Future> call(NoParams params) async { - return await repository.getRandomNumberTrivia(); - } -} \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/presentation/bloc/bloc.dart b/tdd_architeture/lib/features/number_trivia/presentation/bloc/bloc.dart deleted file mode 100644 index 4b27be4..0000000 --- a/tdd_architeture/lib/features/number_trivia/presentation/bloc/bloc.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'number_trivia_bloc.dart'; -export 'number_trivia_event.dart'; -export 'number_trivia_state.dart'; \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/presentation/bloc/number_trivia_bloc.dart b/tdd_architeture/lib/features/number_trivia/presentation/bloc/number_trivia_bloc.dart deleted file mode 100644 index 598ef8d..0000000 --- a/tdd_architeture/lib/features/number_trivia/presentation/bloc/number_trivia_bloc.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:clean_architeture_tdd/core/error/failures.dart'; -import 'package:clean_architeture_tdd/core/usecases/usecase.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/domain/entities/number_trivia.dart'; -import 'package:dartz/dartz.dart'; -import 'package:meta/meta.dart'; - -import './bloc.dart'; -import '../../../../core/util/input_converter.dart'; -import '../../domain/usecases/get_concrete_number_trivia.dart'; -import '../../domain/usecases/get_random_number_trivia.dart'; - -const String SERVER_FAILURE_MESSAGE = 'Server Failure'; -const String CACHE_FAILURE_MESSAGE = 'Cache Failure'; -const String INVALID_INPUT_FAILURE_MESSAGE = - 'Invalid Input - The number must be a positive integer or zero.'; - -class NumberTriviaBloc extends Bloc { - final GetConcreteNumberTrivia getConcreteNumberTrivia; - final GetRandomNumberTrivia getRandomNumberTrivia; - final InputConverter inputConverter; - - NumberTriviaBloc({ - @required GetConcreteNumberTrivia concrete, - @required GetRandomNumberTrivia random, - @required this.inputConverter, - }) : assert(concrete != null), - assert(random != null), - assert(inputConverter != null), - getConcreteNumberTrivia = concrete, - getRandomNumberTrivia = random; - - @override - NumberTriviaState get initialState => Empty(); - - @override - Stream mapEventToState( - NumberTriviaEvent event, - ) async* { - if (event is GetTriviaForConcreteNumber) { - final inputEither = - inputConverter.stringToUnsignedInteger(event.numberString); - - yield* inputEither.fold( - (failure) async* { - yield Error(message: INVALID_INPUT_FAILURE_MESSAGE); - }, - (integer) async* { - yield Loading(); - final failureOrTrivia = - await getConcreteNumberTrivia(Params(number: integer)); - yield* _eitherLoadedOrErrorState(failureOrTrivia); - }, - ); - } else if (event is GetTriviaForRandomNumber) { - yield Loading(); - final failureOrTrivia = await getRandomNumberTrivia(NoParams()); - yield* _eitherLoadedOrErrorState(failureOrTrivia); - } - } - - Stream _eitherLoadedOrErrorState( - Either failureOrTrivia, - ) async* { - yield failureOrTrivia.fold( - (failure) => Error(message: _mapFailureToMessage(failure)), - (trivia) => Loaded(trivia: trivia), - ); - } - - String _mapFailureToMessage(Failure failure) { - switch (failure.runtimeType) { - case ServerFailure: - return SERVER_FAILURE_MESSAGE; - case CacheFailure: - return CACHE_FAILURE_MESSAGE; - default: - return 'Unexpected error'; - } - } -} \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/presentation/bloc/number_trivia_event.dart b/tdd_architeture/lib/features/number_trivia/presentation/bloc/number_trivia_event.dart deleted file mode 100644 index f6ef3a1..0000000 --- a/tdd_architeture/lib/features/number_trivia/presentation/bloc/number_trivia_event.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:meta/meta.dart'; - -@immutable -abstract class NumberTriviaEvent extends Equatable { - @override - List get props => []; -} - -class GetTriviaForConcreteNumber extends NumberTriviaEvent { - final String numberString; - - GetTriviaForConcreteNumber(this.numberString); - - @override - List get props => [numberString]; -} - -class GetTriviaForRandomNumber extends NumberTriviaEvent {} \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/presentation/bloc/number_trivia_state.dart b/tdd_architeture/lib/features/number_trivia/presentation/bloc/number_trivia_state.dart deleted file mode 100644 index feb5ce4..0000000 --- a/tdd_architeture/lib/features/number_trivia/presentation/bloc/number_trivia_state.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:clean_architeture_tdd/features/number_trivia/domain/entities/number_trivia.dart'; -import 'package:equatable/equatable.dart'; -import 'package:meta/meta.dart'; - -@immutable -abstract class NumberTriviaState extends Equatable { - @override - List get props => []; -} - -class Empty extends NumberTriviaState {} - -class Loading extends NumberTriviaState {} - -class Loaded extends NumberTriviaState { - final NumberTrivia trivia; - - Loaded({@required this.trivia}); - - @override - List get props => [trivia]; -} - -class Error extends NumberTriviaState { - final String message; - - Error({@required this.message}); - - @override - List get props => [message]; -} \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/presentation/pages/number_trivia_page.dart b/tdd_architeture/lib/features/number_trivia/presentation/pages/number_trivia_page.dart deleted file mode 100644 index 522b18b..0000000 --- a/tdd_architeture/lib/features/number_trivia/presentation/pages/number_trivia_page.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:clean_architeture_tdd/features/number_trivia/presentation/bloc/bloc.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/presentation/bloc/number_trivia_bloc.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/presentation/widgets/widgets.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../../../injection_container.dart'; - -class NumberTriviaPage extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Number Trivia'), - ), - body: SingleChildScrollView( - child: buildBody(context), - ), - ); - } - - BlocProvider buildBody(BuildContext context) { - return BlocProvider( - create: (_) => sl(), - child: Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: Column( - children: [ - SizedBox(height: 10), - // Top half - BlocBuilder( - builder: (context, state) { - if (state is Empty) { - return MessageDisplay( - message: 'Start searching!', - ); - } else if (state is Loading) { - return LoadingWidget(); - } else if (state is Loaded) { - return TriviaDisplay(numberTrivia: state.trivia); - } else if (state is Error) { - return MessageDisplay( - message: state.message, - ); - } - }, - ), - SizedBox(height: 20), - // Bottom half - TriviaControls() - ], - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/presentation/widgets/loading_widget.dart b/tdd_architeture/lib/features/number_trivia/presentation/widgets/loading_widget.dart deleted file mode 100644 index 7959911..0000000 --- a/tdd_architeture/lib/features/number_trivia/presentation/widgets/loading_widget.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -class LoadingWidget extends StatelessWidget { - const LoadingWidget({ - Key key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - height: MediaQuery.of(context).size.height / 3, - child: Center( - child: CircularProgressIndicator(), - ), - ); - } -} \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/presentation/widgets/message_display.dart b/tdd_architeture/lib/features/number_trivia/presentation/widgets/message_display.dart deleted file mode 100644 index c7a30c6..0000000 --- a/tdd_architeture/lib/features/number_trivia/presentation/widgets/message_display.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; - -class MessageDisplay extends StatelessWidget { - final String message; - - const MessageDisplay({ - Key key, - @required this.message, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - height: MediaQuery.of(context).size.height / 3, - child: Center( - child: SingleChildScrollView( - child: Text( - message, - style: TextStyle(fontSize: 25), - textAlign: TextAlign.center, - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/presentation/widgets/trivia_controls.dart b/tdd_architeture/lib/features/number_trivia/presentation/widgets/trivia_controls.dart deleted file mode 100644 index 2a52109..0000000 --- a/tdd_architeture/lib/features/number_trivia/presentation/widgets/trivia_controls.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:clean_architeture_tdd/features/number_trivia/presentation/bloc/bloc.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class TriviaControls extends StatefulWidget { - const TriviaControls({ - Key key, - }) : super(key: key); - - @override - _TriviaControlsState createState() => _TriviaControlsState(); -} - -class _TriviaControlsState extends State { - final controller = TextEditingController(); - String inputStr; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - TextField( - controller: controller, - keyboardType: TextInputType.number, - decoration: InputDecoration( - border: OutlineInputBorder(), - hintText: 'Input a number', - ), - onChanged: (value) { - inputStr = value; - }, - onSubmitted: (_) { - dispatchConcrete(); - }, - ), - SizedBox(height: 10), - Row( - children: [ - Expanded( - child: RaisedButton( - child: Text('Search'), - color: Theme.of(context).accentColor, - textTheme: ButtonTextTheme.primary, - onPressed: dispatchConcrete, - ), - ), - SizedBox(width: 10), - Expanded( - child: RaisedButton( - child: Text('Get random trivia'), - onPressed: dispatchRandom, - ), - ), - ], - ) - ], - ); - } - - void dispatchConcrete() { - controller.clear(); - BlocProvider.of(context) - .add(GetTriviaForConcreteNumber(inputStr)); - } - - void dispatchRandom() { - controller.clear(); - BlocProvider.of(context).add(GetTriviaForRandomNumber()); - } -} \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/presentation/widgets/trivia_display.dart b/tdd_architeture/lib/features/number_trivia/presentation/widgets/trivia_display.dart deleted file mode 100644 index 48d3c80..0000000 --- a/tdd_architeture/lib/features/number_trivia/presentation/widgets/trivia_display.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:clean_architeture_tdd/features/number_trivia/domain/entities/number_trivia.dart'; -import 'package:flutter/material.dart'; - -class TriviaDisplay extends StatelessWidget { - final NumberTrivia numberTrivia; - - const TriviaDisplay({ - Key key, - @required this.numberTrivia, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - height: MediaQuery.of(context).size.height / 3, - child: Column( - children: [ - Text( - numberTrivia.number.toString(), - style: TextStyle(fontSize: 50, fontWeight: FontWeight.bold), - ), - Expanded( - child: Center( - child: SingleChildScrollView( - child: Text( - numberTrivia.text, - style: TextStyle(fontSize: 25), - textAlign: TextAlign.center, - ), - ), - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/tdd_architeture/lib/features/number_trivia/presentation/widgets/widgets.dart b/tdd_architeture/lib/features/number_trivia/presentation/widgets/widgets.dart deleted file mode 100644 index e7801ce..0000000 --- a/tdd_architeture/lib/features/number_trivia/presentation/widgets/widgets.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'loading_widget.dart'; -export 'message_display.dart'; -export 'trivia_display.dart'; -export 'trivia_controls.dart'; \ No newline at end of file diff --git a/tdd_architeture/lib/injection_container.dart b/tdd_architeture/lib/injection_container.dart deleted file mode 100644 index eb00ca3..0000000 --- a/tdd_architeture/lib/injection_container.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:clean_architeture_tdd/features/number_trivia/domain/number_trivia_repository.dart'; -import 'package:data_connection_checker/data_connection_checker.dart'; -import 'package:get_it/get_it.dart'; -import 'package:http/http.dart' as http; -import 'package:shared_preferences/shared_preferences.dart'; - -import 'core/network/network_info.dart'; -import 'core/util/input_converter.dart'; -import 'features/number_trivia/data/datasources/number_trivia_local_data_source.dart'; -import 'features/number_trivia/data/datasources/number_trivia_remote_data_source.dart'; -import 'features/number_trivia/data/repositories/number_trivia_repository_impl.dart'; -import 'features/number_trivia/domain/usecases/get_concrete_number_trivia.dart'; -import 'features/number_trivia/domain/usecases/get_random_number_trivia.dart'; -import 'features/number_trivia/presentation/bloc/number_trivia_bloc.dart'; - -final sl = GetIt.instance; - -Future init() async { - //! Features - Number Trivia - // Bloc - sl.registerFactory( - () => NumberTriviaBloc( - concrete: sl(), - inputConverter: sl(), - random: sl(), - ), - ); - - // Use cases - sl.registerLazySingleton(() => GetConcreteNumberTrivia(sl())); - sl.registerLazySingleton(() => GetRandomNumberTrivia(sl())); - - // Repository - sl.registerLazySingleton( - () => NumberTriviaRepositoryImpl( - localDataSource: sl(), - networkInfo: sl(), - remoteDataSource: sl(), - ), - ); - - // Data sources - sl.registerLazySingleton( - () => NumberTriviaRemoteDataSourceImpl(client: sl()), - ); - - sl.registerLazySingleton( - () => NumberTriviaLocalDataSourceImpl(sharedPreferences: sl()), - ); - - //! Core - sl.registerLazySingleton(() => InputConverter()); - sl.registerLazySingleton(() => NetworkInfoImpl(sl())); - - //! External - final sharedPreferences = await SharedPreferences.getInstance(); - sl.registerLazySingleton(() => sharedPreferences); - sl.registerLazySingleton(() => http.Client()); - sl.registerLazySingleton(() => DataConnectionChecker()); -} \ No newline at end of file diff --git a/tdd_architeture/lib/main.dart b/tdd_architeture/lib/main.dart deleted file mode 100644 index 1c17c0a..0000000 --- a/tdd_architeture/lib/main.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:clean_architeture_tdd/features/number_trivia/presentation/pages/number_trivia_page.dart'; -import 'package:flutter/material.dart'; -import 'injection_container.dart' as di; - -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - await di.init(); - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Number Trivia', - theme: ThemeData( - primaryColor: Colors.green.shade800, - accentColor: Colors.green.shade600, - ), - home: NumberTriviaPage(), - ); - } -} \ No newline at end of file diff --git a/tdd_architeture/pubspec.yaml b/tdd_architeture/pubspec.yaml deleted file mode 100644 index d3f93e5..0000000 --- a/tdd_architeture/pubspec.yaml +++ /dev/null @@ -1,76 +0,0 @@ -name: clean_architeture_tdd -description: A new Flutter application. - - -version: 1.0.0+1 - -environment: - sdk: ">=2.1.0 <3.0.0" - -dependencies: - flutter: - sdk: flutter - - get_it: ^2.0.1 - # Bloc for state management - flutter_bloc: ^3.2.0 - # Value equality - equatable: ^1.1.0 - # Functional programming thingies - dartz: ^0.8.6 - # Remote API - connectivity: ^0.4.3+7 - http: ^0.12.0+2 - # Local cache - shared_preferences: ^0.5.3+4 - data_connection_checker: ^0.3.4 - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 - -dev_dependencies: - flutter_test: - sdk: flutter - mockito: ^4.1.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/tdd_architeture/test/core/network/network_info_test.dart b/tdd_architeture/test/core/network/network_info_test.dart deleted file mode 100644 index 5630858..0000000 --- a/tdd_architeture/test/core/network/network_info_test.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:clean_architeture_tdd/core/network/network_info.dart'; -import 'package:mockito/mockito.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:data_connection_checker/data_connection_checker.dart'; - -class MockDataConnectionChecker extends Mock implements DataConnectionChecker {} - -void main() { - NetworkInfoImpl networkInfo; - MockDataConnectionChecker mockDataConnectionChecker; - - setUp(() { - mockDataConnectionChecker = MockDataConnectionChecker(); - networkInfo = NetworkInfoImpl(mockDataConnectionChecker); - }); - - group('isConnected', () { - test( - 'should forward the call to DataConnectionChecker.hasConnection', - () async { - // arrange - final tHasConnectionFuture = Future.value(true); - - when(mockDataConnectionChecker.hasConnection) - .thenAnswer((_) => tHasConnectionFuture); - // act - final result = networkInfo.isConnected; - // assert - verify(mockDataConnectionChecker.hasConnection); - expect(result, tHasConnectionFuture); - }, - ); - }); -} \ No newline at end of file diff --git a/tdd_architeture/test/core/util/input_converter_test.dart b/tdd_architeture/test/core/util/input_converter_test.dart deleted file mode 100644 index 7db8911..0000000 --- a/tdd_architeture/test/core/util/input_converter_test.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:clean_architeture_tdd/core/util/input_converter.dart'; -import 'package:dartz/dartz.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - InputConverter inputConverter; - - setUp(() { - inputConverter = InputConverter(); - }); - - group('stringToUnsignedInt', () { - test( - 'should return an integer when the string represents an unsigned integer', - () async { - // arrange - final str = '123'; - // act - final result = inputConverter.stringToUnsignedInteger(str); - // assert - expect(result, Right(123)); - }, - ); - - test( - 'should return a Failure when the string is not an integer', - () async { - // arrange - final str = 'abc'; - // act - final result = inputConverter.stringToUnsignedInteger(str); - // assert - expect(result, Left(InvalidInputFailure())); - }, - ); - - test( - 'should return a Failure when the string is a negative integer', - () async { - // arrange - final str = '-123'; - // act - final result = inputConverter.stringToUnsignedInteger(str); - // assert - expect(result, Left(InvalidInputFailure())); - }, - ); - }); -} \ No newline at end of file diff --git a/tdd_architeture/test/features/number_trivia/Data/datasources/number_trivia_local_data_source_test.dart b/tdd_architeture/test/features/number_trivia/Data/datasources/number_trivia_local_data_source_test.dart deleted file mode 100644 index 0c67fa2..0000000 --- a/tdd_architeture/test/features/number_trivia/Data/datasources/number_trivia_local_data_source_test.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:convert'; - -import 'package:clean_architeture_tdd/core/error/exceptions.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/data/datasources/number_trivia_local_data_source.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/data/models/number_trivia_model.dart'; -import 'package:matcher/matcher.dart'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../../../../fixtures/Fixture_reader.dart'; - -class MockSharedPreferences extends Mock implements SharedPreferences {} - -void main() { - NumberTriviaLocalDataSourceImpl dataSource; - MockSharedPreferences mockSharedPreferences; - - setUp(() { - mockSharedPreferences = MockSharedPreferences(); - dataSource = NumberTriviaLocalDataSourceImpl( - sharedPreferences: mockSharedPreferences, - ); - }); - group('getLastNumberTrivia', () { - final tNumberTriviaModel = - NumberTriviaModel.fromJson(json.decode(fixture('trivia_cached.json'))); - - test( - 'should return NumberTrivia from SharedPreferences when there is one in the cache', - () async { - // arrange - when(mockSharedPreferences.getString(any)) - .thenReturn(fixture('trivia_cached.json')); - // act - final result = await dataSource.getLastNumberTrivia(); - // assert - verify(mockSharedPreferences.getString('CACHED_NUMBER_TRIVIA')); - expect(result, equals(tNumberTriviaModel)); - }, - ); -}); -test('should throw a CacheException when there is not a cached value', () { - // arrange - when(mockSharedPreferences.getString(any)).thenReturn(null); - // act - // Not calling the method here, just storing it inside a call variable - final call = dataSource.getLastNumberTrivia; - // assert - // Calling the method happens from a higher-order function passed. - // This is needed to test if calling a method throws an exception. - expect(() => call(), throwsA(TypeMatcher())); -}); -group('cacheNumberTrivia', () { - final tNumberTriviaModel = - NumberTriviaModel(number: 1, text: 'test trivia'); - - test('should call SharedPreferences to cache the data', () { - // act - dataSource.cacheNumberTrivia(tNumberTriviaModel); - // assert - final expectedJsonString = json.encode(tNumberTriviaModel.toJson()); - verify(mockSharedPreferences.setString(CACHED_NUMBER_TRIVIA,expectedJsonString,)); - }); -}); -} \ No newline at end of file diff --git a/tdd_architeture/test/features/number_trivia/Data/datasources/number_trivia_remote_data_source_test.dart b/tdd_architeture/test/features/number_trivia/Data/datasources/number_trivia_remote_data_source_test.dart deleted file mode 100644 index 9f8a1ed..0000000 --- a/tdd_architeture/test/features/number_trivia/Data/datasources/number_trivia_remote_data_source_test.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'dart:convert'; - -import 'package:clean_architeture_tdd/core/error/exceptions.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/data/models/number_trivia_model.dart'; -import 'package:mockito/mockito.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:matcher/matcher.dart'; -import 'package:http/http.dart' as http; - -import '../../../../fixtures/fixture_reader.dart'; - -class MockHttpClient extends Mock implements http.Client {} - -void main() { - NumberTriviaRemoteDataSourceImpl dataSource; - MockHttpClient mockHttpClient; - - setUp(() { - mockHttpClient = MockHttpClient(); - dataSource = NumberTriviaRemoteDataSourceImpl(client: mockHttpClient); - }); - - void setUpMockHttpClientSuccess200() { - when(mockHttpClient.get(any, headers: anyNamed('headers'))) - .thenAnswer((_) async => http.Response(fixture('trivia.json'), 200)); - } - - void setUpMockHttpClientFailure404() { - when(mockHttpClient.get(any, headers: anyNamed('headers'))) - .thenAnswer((_) async => http.Response('Something went wrong', 404)); - } - - group('getConcreteNumberTrivia', () { - final tNumber = 1; - final tNumberTriviaModel = - NumberTriviaModel.fromJson(json.decode(fixture('trivia.json'))); - - test( - '''should perform a GET request on a URL with number - being the endpoint and with application/json header''', - () async { - // arrange - setUpMockHttpClientSuccess200(); - // act - dataSource.getConcreteNumberTrivia(tNumber); - // assert - verify(mockHttpClient.get( - 'http://numbersapi.com/$tNumber', - headers: { - 'Content-Type': 'application/json', - }, - )); - }, - ); - - test( - 'should return NumberTrivia when the response code is 200 (success)', - () async { - // arrange - setUpMockHttpClientSuccess200(); - // act - final result = await dataSource.getConcreteNumberTrivia(tNumber); - // assert - expect(result, equals(tNumberTriviaModel)); - }, - ); - - test( - 'should throw a ServerException when the response code is 404 or other', - () async { - // arrange - setUpMockHttpClientFailure404(); - // act - final call = dataSource.getConcreteNumberTrivia; - // assert - expect(() => call(tNumber), throwsA(TypeMatcher())); - }, - ); - }); - - group('getRandomNumberTrivia', () { - final tNumberTriviaModel = - NumberTriviaModel.fromJson(json.decode(fixture('trivia.json'))); - - test( - '''should perform a GET request on a URL with number - being the endpoint and with application/json header''', - () async { - // arrange - setUpMockHttpClientSuccess200(); - // act - dataSource.getRandomNumberTrivia(); - // assert - verify(mockHttpClient.get( - 'http://numbersapi.com/random', - headers: { - 'Content-Type': 'application/json', - }, - )); - }, - ); - - test( - 'should return NumberTrivia when the response code is 200 (success)', - () async { - // arrange - setUpMockHttpClientSuccess200(); - // act - final result = await dataSource.getRandomNumberTrivia(); - // assert - expect(result, equals(tNumberTriviaModel)); - }, - ); - - test( - 'should throw a ServerException when the response code is 404 or other', - () async { - // arrange - setUpMockHttpClientFailure404(); - // act - final call = dataSource.getRandomNumberTrivia; - // assert - expect(() => call(), throwsA(TypeMatcher())); - }, - ); - }); -} \ No newline at end of file diff --git a/tdd_architeture/test/features/number_trivia/Data/models/number_trivia_model_test.dart b/tdd_architeture/test/features/number_trivia/Data/models/number_trivia_model_test.dart deleted file mode 100644 index 38eb038..0000000 --- a/tdd_architeture/test/features/number_trivia/Data/models/number_trivia_model_test.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'dart:convert'; -import 'package:clean_architeture_tdd/features/number_trivia/data/models/number_trivia_model.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/domain/entities/number_trivia.dart'; -import 'package:flutter_test/flutter_test.dart'; -import '../../../../fixtures/Fixture_reader.dart'; - - -void main() { - final tNumberTriviaModel = NumberTriviaModel(number: 1, text: 'Test Text'); - - test( - 'should be a subclass of NumberTrivia Entity', - () async { - - expect(tNumberTriviaModel, isA()); - - - }, - ); - - group('fromJson', () { - - test( - 'should return a valid model when the JSON number is regarded as a double', - () async { - // arrange - final Map jsonMap = - json.decode(fixture('trivia_double.json')); - // act - final result = NumberTriviaModel.fromJson(jsonMap); - // assert - expect(result, tNumberTriviaModel); - }, - ); -}); -} \ No newline at end of file diff --git a/tdd_architeture/test/features/number_trivia/Data/repositories/number_trivia_repository_impl_test.dart b/tdd_architeture/test/features/number_trivia/Data/repositories/number_trivia_repository_impl_test.dart deleted file mode 100644 index 41e74fa..0000000 --- a/tdd_architeture/test/features/number_trivia/Data/repositories/number_trivia_repository_impl_test.dart +++ /dev/null @@ -1,247 +0,0 @@ -import 'package:clean_architeture_tdd/core/error/exceptions.dart'; -import 'package:clean_architeture_tdd/core/error/failures.dart'; -import 'package:clean_architeture_tdd/core/network/network_info.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/data/datasources/number_trivia_local_data_source.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/data/models/number_trivia_model.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/data/repositories/number_trivia_repository_impl.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/domain/entities/number_trivia.dart'; -import 'package:dartz/dartz.dart'; -import 'package:mockito/mockito.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class MockRemoteDataSource extends Mock - implements NumberTriviaRemoteDataSource {} - -class MockLocalDataSource extends Mock implements NumberTriviaLocalDataSource {} - -class MockNetworkInfo extends Mock implements NetworkInfo {} - -void main() { - NumberTriviaRepositoryImpl repository; - MockRemoteDataSource mockRemoteDataSource; - MockLocalDataSource mockLocalDataSource; - MockNetworkInfo mockNetworkInfo; - - setUp(() { - mockRemoteDataSource = MockRemoteDataSource(); - mockLocalDataSource = MockLocalDataSource(); - mockNetworkInfo = MockNetworkInfo(); - repository = NumberTriviaRepositoryImpl( - remoteDataSource: mockRemoteDataSource, - localDataSource: mockLocalDataSource, - networkInfo: mockNetworkInfo, - ); - }); - - void runTestsOnline(Function body) { - group('device is online', () { - setUp(() { - when(mockNetworkInfo.isConnected).thenAnswer((_) async => true); - }); - - body(); - }); - } - - void runTestsOffline(Function body) { - group('device is offline', () { - setUp(() { - when(mockNetworkInfo.isConnected).thenAnswer((_) async => false); - }); - - body(); - }); - } - - group('getConcreteNumberTrivia', () { - final tNumber = 1; - final tNumberTriviaModel = - NumberTriviaModel(number: tNumber, text: 'test trivia'); - final NumberTrivia tNumberTrivia = tNumberTriviaModel; - - test( - 'should check if the device is online', - () async { - // arrange - when(mockNetworkInfo.isConnected).thenAnswer((_) async => true); - // act - repository.getConcreteNumberTrivia(tNumber); - // assert - verify(mockNetworkInfo.isConnected); - }, - ); - - runTestsOnline(() { - test( - 'should return remote data when the call to remote data source is successful', - () async { - // arrange - when(mockRemoteDataSource.getConcreteNumberTrivia(any)) - .thenAnswer((_) async => tNumberTriviaModel); - // act - final result = await repository.getConcreteNumberTrivia(tNumber); - // assert - verify(mockRemoteDataSource.getConcreteNumberTrivia(tNumber)); - expect(result, equals(Right(tNumberTrivia))); - }, - ); - - test( - 'should cache the data locally when the call to remote data source is successful', - () async { - // arrange - when(mockRemoteDataSource.getConcreteNumberTrivia(any)) - .thenAnswer((_) async => tNumberTriviaModel); - // act - await repository.getConcreteNumberTrivia(tNumber); - // assert - verify(mockRemoteDataSource.getConcreteNumberTrivia(tNumber)); - verify(mockLocalDataSource.cacheNumberTrivia(tNumberTriviaModel)); - }, - ); - - test( - 'should return server failure when the call to remote data source is unsuccessful', - () async { - // arrange - when(mockRemoteDataSource.getConcreteNumberTrivia(any)) - .thenThrow(ServerException()); - // act - final result = await repository.getConcreteNumberTrivia(tNumber); - // assert - verify(mockRemoteDataSource.getConcreteNumberTrivia(tNumber)); - verifyZeroInteractions(mockLocalDataSource); - expect(result, equals(Left(ServerFailure()))); - }, - ); - }); - - runTestsOffline(() { - test( - 'should return last locally cached data when the cached data is present', - () async { - // arrange - when(mockLocalDataSource.getLastNumberTrivia()) - .thenAnswer((_) async => tNumberTriviaModel); - // act - final result = await repository.getConcreteNumberTrivia(tNumber); - // assert - verifyZeroInteractions(mockRemoteDataSource); - verify(mockLocalDataSource.getLastNumberTrivia()); - expect(result, equals(Right(tNumberTrivia))); - }, - ); - - test( - 'should return CacheFailure when there is no cached data present', - () async { - // arrange - when(mockLocalDataSource.getLastNumberTrivia()) - .thenThrow(CacheException()); - // act - final result = await repository.getConcreteNumberTrivia(tNumber); - // assert - verifyZeroInteractions(mockRemoteDataSource); - verify(mockLocalDataSource.getLastNumberTrivia()); - expect(result, equals(Left(CacheFailure()))); - }, - ); - }); - }); - - group('getRandomNumberTrivia', () { - final tNumberTriviaModel = - NumberTriviaModel(number: 123, text: 'test trivia'); - final NumberTrivia tNumberTrivia = tNumberTriviaModel; - - test( - 'should check if the device is online', - () async { - // arrange - when(mockNetworkInfo.isConnected).thenAnswer((_) async => true); - // act - repository.getRandomNumberTrivia(); - // assert - verify(mockNetworkInfo.isConnected); - }, - ); - - runTestsOnline(() { - test( - 'should return remote data when the call to remote data source is successful', - () async { - // arrange - when(mockRemoteDataSource.getRandomNumberTrivia()) - .thenAnswer((_) async => tNumberTriviaModel); - // act - final result = await repository.getRandomNumberTrivia(); - // assert - verify(mockRemoteDataSource.getRandomNumberTrivia()); - expect(result, equals(Right(tNumberTrivia))); - }, - ); - - test( - 'should cache the data locally when the call to remote data source is successful', - () async { - // arrange - when(mockRemoteDataSource.getRandomNumberTrivia()) - .thenAnswer((_) async => tNumberTriviaModel); - // act - await repository.getRandomNumberTrivia(); - // assert - verify(mockRemoteDataSource.getRandomNumberTrivia()); - verify(mockLocalDataSource.cacheNumberTrivia(tNumberTriviaModel)); - }, - ); - - test( - 'should return server failure when the call to remote data source is unsuccessful', - () async { - // arrange - when(mockRemoteDataSource.getRandomNumberTrivia()) - .thenThrow(ServerException()); - // act - final result = await repository.getRandomNumberTrivia(); - // assert - verify(mockRemoteDataSource.getRandomNumberTrivia()); - verifyZeroInteractions(mockLocalDataSource); - expect(result, equals(Left(ServerFailure()))); - }, - ); - }); - - runTestsOffline(() { - test( - 'should return last locally cached data when the cached data is present', - () async { - // arrange - when(mockLocalDataSource.getLastNumberTrivia()) - .thenAnswer((_) async => tNumberTriviaModel); - // act - final result = await repository.getRandomNumberTrivia(); - // assert - verifyZeroInteractions(mockRemoteDataSource); - verify(mockLocalDataSource.getLastNumberTrivia()); - expect(result, equals(Right(tNumberTrivia))); - }, - ); - - test( - 'should return CacheFailure when there is no cached data present', - () async { - // arrange - when(mockLocalDataSource.getLastNumberTrivia()) - .thenThrow(CacheException()); - // act - final result = await repository.getRandomNumberTrivia(); - // assert - verifyZeroInteractions(mockRemoteDataSource); - verify(mockLocalDataSource.getLastNumberTrivia()); - expect(result, equals(Left(CacheFailure()))); - }, - ); - }); - }); -} \ No newline at end of file diff --git a/tdd_architeture/test/features/number_trivia/Domain/usescases/get_concrete_number_trivia_test.dart b/tdd_architeture/test/features/number_trivia/Domain/usescases/get_concrete_number_trivia_test.dart deleted file mode 100644 index 5c3cf67..0000000 --- a/tdd_architeture/test/features/number_trivia/Domain/usescases/get_concrete_number_trivia_test.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:clean_architeture_tdd/features/number_trivia/domain/entities/number_trivia.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/domain/number_trivia_repository.dart'; -import 'package:dartz/dartz.dart'; -import 'package:mockito/mockito.dart'; -import 'package:flutter_test/flutter_test.dart'; - - -class MockNumberTriviaRepository extends Mock implements NumberTriviaRepository {} - -void main(){ - GetConcreteNumberTrivia usecase; - MockNumberTriviaRepository mockNumberTriviaRepository; - setUp((){ - mockNumberTriviaRepository = MockNumberTriviaRepository(); - usecase = GetConcreteNumberTrivia(mockNumberTriviaRepository); - }); - -final tNumber = 1; -final tNumberTrivia = NumberTrivia(number: 1, text: 'test'); - - test( - 'should get trivia for the number for the repository', - () async{ -when (mockNumberTriviaRepository.getConcreteNumberTrivia(any)) -.thenAnswer((_) async => Right(tNumberTrivia)); - - final result = await usecase(Params(number: tNumber)); - - expect(result, Right(tNumberTrivia)); - - verify(mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber)); - - verifyNoMoreInteractions(mockNumberTriviaRepository); - - }, - ); -} \ No newline at end of file diff --git a/tdd_architeture/test/features/number_trivia/Domain/usescases/get_random_number_trivia_test.dart b/tdd_architeture/test/features/number_trivia/Domain/usescases/get_random_number_trivia_test.dart deleted file mode 100644 index 83abd81..0000000 --- a/tdd_architeture/test/features/number_trivia/Domain/usescases/get_random_number_trivia_test.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:clean_architeture_tdd/core/usecases/usecase.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/domain/entities/number_trivia.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/domain/number_trivia_repository.dart'; - -import 'package:clean_architeture_tdd/features/number_trivia/domain/usecases/get_random_number_trivia.dart'; -import 'package:dartz/dartz.dart'; -import 'package:mockito/mockito.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class MockNumberTriviaRepository extends Mock - implements NumberTriviaRepository {} - -void main() { - GetRandomNumberTrivia usecase; - MockNumberTriviaRepository mockNumberTriviaRepository; - - setUp(() { - mockNumberTriviaRepository = MockNumberTriviaRepository(); - usecase = GetRandomNumberTrivia(mockNumberTriviaRepository); - }); - - final tNumberTrivia = NumberTrivia(number: 1, text: 'test'); - - test( - 'should get trivia from the repository', - () async { - // arrange - when(mockNumberTriviaRepository.getRandomNumberTrivia()) - .thenAnswer((_) async => Right(tNumberTrivia)); - // act - final result = await usecase(NoParams()); - // assert - expect(result, Right(tNumberTrivia)); - verify(mockNumberTriviaRepository.getRandomNumberTrivia()); - verifyNoMoreInteractions(mockNumberTriviaRepository); - }, - ); -} \ No newline at end of file diff --git a/tdd_architeture/test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart b/tdd_architeture/test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart deleted file mode 100644 index 330c6d3..0000000 --- a/tdd_architeture/test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart +++ /dev/null @@ -1,225 +0,0 @@ -import 'package:clean_architeture_tdd/core/error/failures.dart'; -import 'package:clean_architeture_tdd/core/usecases/usecase.dart'; -import 'package:clean_architeture_tdd/core/util/input_converter.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/domain/entities/number_trivia.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/domain/usecases/get_random_number_trivia.dart'; -import 'package:clean_architeture_tdd/features/number_trivia/presentation/bloc/bloc.dart'; -import 'package:dartz/dartz.dart'; -import 'package:mockito/mockito.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class MockGetConcreteNumberTrivia extends Mock - implements GetConcreteNumberTrivia {} - -class MockGetRandomNumberTrivia extends Mock implements GetRandomNumberTrivia {} - -class MockInputConverter extends Mock implements InputConverter {} - -void main() { - NumberTriviaBloc bloc; - MockGetConcreteNumberTrivia mockGetConcreteNumberTrivia; - MockGetRandomNumberTrivia mockGetRandomNumberTrivia; - MockInputConverter mockInputConverter; - - setUp(() { - mockGetConcreteNumberTrivia = MockGetConcreteNumberTrivia(); - mockGetRandomNumberTrivia = MockGetRandomNumberTrivia(); - mockInputConverter = MockInputConverter(); - - bloc = NumberTriviaBloc( - concrete: mockGetConcreteNumberTrivia, - random: mockGetRandomNumberTrivia, - inputConverter: mockInputConverter, - ); - }); - - test('initialState should be Empty', () { - // assert - expect(bloc.initialState, equals(Empty())); - }); - - group('GetTriviaForConcreteNumber', () { - final tNumberString = '1'; - final tNumberParsed = 1; - final tNumberTrivia = NumberTrivia(number: 1, text: 'test trivia'); - - void setUpMockInputConverterSuccess() => - when(mockInputConverter.stringToUnsignedInteger(any)) - .thenReturn(Right(tNumberParsed)); - - test( - 'should call the InputConverter to validate and convert the string to an unsigned integer', - () async { - // arrange - setUpMockInputConverterSuccess(); - // act - bloc.add(GetTriviaForConcreteNumber(tNumberString)); - await untilCalled(mockInputConverter.stringToUnsignedInteger(any)); - // assert - verify(mockInputConverter.stringToUnsignedInteger(tNumberString)); - }, - ); - - test( - 'should emit [Error] when the input is invalid', - () async { - // arrange - when(mockInputConverter.stringToUnsignedInteger(any)) - .thenReturn(Left(InvalidInputFailure())); - // assert later - final expected = [ - Empty(), - Error(message: INVALID_INPUT_FAILURE_MESSAGE), - ]; - expectLater(bloc, emitsInOrder(expected)); - // act - bloc.add(GetTriviaForConcreteNumber(tNumberString)); - }, - ); - - test( - 'should get data from the concrete use case', - () async { - // arrange - setUpMockInputConverterSuccess(); - when(mockGetConcreteNumberTrivia(any)) - .thenAnswer((_) async => Right(tNumberTrivia)); - // act - bloc.add(GetTriviaForConcreteNumber(tNumberString)); - await untilCalled(mockGetConcreteNumberTrivia(any)); - // assert - verify(mockGetConcreteNumberTrivia(Params(number: tNumberParsed))); - }, - ); - - test( - 'should emit [Loading, Loaded] when data is gotten successfully', - () async { - // arrange - setUpMockInputConverterSuccess(); - when(mockGetConcreteNumberTrivia(any)) - .thenAnswer((_) async => Right(tNumberTrivia)); - // assert later - final expected = [ - Empty(), - Loading(), - Loaded(trivia: tNumberTrivia), - ]; - expectLater(bloc, emitsInOrder(expected)); - // act - bloc.add(GetTriviaForConcreteNumber(tNumberString)); - }, - ); - - test( - 'should emit [Loading, Error] when getting data fails', - () async { - // arrange - setUpMockInputConverterSuccess(); - when(mockGetConcreteNumberTrivia(any)) - .thenAnswer((_) async => Left(ServerFailure())); - // assert later - final expected = [ - Empty(), - Loading(), - Error(message: SERVER_FAILURE_MESSAGE), - ]; - expectLater(bloc, emitsInOrder(expected)); - // act - bloc.add(GetTriviaForConcreteNumber(tNumberString)); - }, - ); - - test( - 'should emit [Loading, Error] with a proper message for the error when getting data fails', - () async { - // arrange - setUpMockInputConverterSuccess(); - when(mockGetConcreteNumberTrivia(any)) - .thenAnswer((_) async => Left(CacheFailure())); - // assert later - final expected = [ - Empty(), - Loading(), - Error(message: CACHE_FAILURE_MESSAGE), - ]; - expectLater(bloc, emitsInOrder(expected)); - // act - bloc.add(GetTriviaForConcreteNumber(tNumberString)); - }, - ); - }); - - group('GetTriviaForRandomNumber', () { - final tNumberTrivia = NumberTrivia(number: 1, text: 'test trivia'); - - test( - 'should get data from the random use case', - () async { - // arrange - when(mockGetRandomNumberTrivia(any)) - .thenAnswer((_) async => Right(tNumberTrivia)); - // act - bloc.add(GetTriviaForRandomNumber()); - await untilCalled(mockGetRandomNumberTrivia(any)); - // assert - verify(mockGetRandomNumberTrivia(NoParams())); - }, - ); - - test( - 'should emit [Loading, Loaded] when data is gotten successfully', - () async { - // arrange - when(mockGetRandomNumberTrivia(any)) - .thenAnswer((_) async => Right(tNumberTrivia)); - // assert later - final expected = [ - Empty(), - Loading(), - Loaded(trivia: tNumberTrivia), - ]; - expectLater(bloc, emitsInOrder(expected)); - // act - bloc.add(GetTriviaForRandomNumber()); - }, - ); - - test( - 'should emit [Loading, Error] when getting data fails', - () async { - // arrange - when(mockGetRandomNumberTrivia(any)) - .thenAnswer((_) async => Left(ServerFailure())); - // assert later - final expected = [ - Empty(), - Loading(), - Error(message: SERVER_FAILURE_MESSAGE), - ]; - expectLater(bloc, emitsInOrder(expected)); - // act - bloc.add(GetTriviaForRandomNumber()); - }, - ); - - test( - 'should emit [Loading, Error] with a proper message for the error when getting data fails', - () async { - // arrange - when(mockGetRandomNumberTrivia(any)) - .thenAnswer((_) async => Left(CacheFailure())); - // assert later - final expected = [ - Empty(), - Loading(), - Error(message: CACHE_FAILURE_MESSAGE), - ]; - expectLater(bloc, emitsInOrder(expected)); - // act - bloc.add(GetTriviaForRandomNumber()); - }, - ); - }); -} \ No newline at end of file diff --git a/tdd_architeture/test/fixtures/Fixture_reader.dart b/tdd_architeture/test/fixtures/Fixture_reader.dart deleted file mode 100644 index fcb2f09..0000000 --- a/tdd_architeture/test/fixtures/Fixture_reader.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'dart:io'; - -String fixture(String name) => File('test/fixtures/$name').readAsStringSync(); diff --git a/tdd_architeture/test/fixtures/trivia.json b/tdd_architeture/test/fixtures/trivia.json deleted file mode 100644 index 2dec90f..0000000 --- a/tdd_architeture/test/fixtures/trivia.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "text": "Test Text", - "number": 1, - "found": true, - "type": "trivia" - } \ No newline at end of file diff --git a/tdd_architeture/test/fixtures/trivia_cached.json b/tdd_architeture/test/fixtures/trivia_cached.json deleted file mode 100644 index c9ae797..0000000 --- a/tdd_architeture/test/fixtures/trivia_cached.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "text": "Test Text", - "number": 1 - } \ No newline at end of file diff --git a/tdd_architeture/test/fixtures/trivia_double.json b/tdd_architeture/test/fixtures/trivia_double.json deleted file mode 100644 index c137b35..0000000 --- a/tdd_architeture/test/fixtures/trivia_double.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "text": "Test Text", - "number": 1.0, - "found": true, - "type": "trivia" - } \ No newline at end of file From 68b3f07c4012997cd389205269d30e18e73cbab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Thu, 10 Nov 2022 13:20:06 +0000 Subject: [PATCH 23/33] feat: Adding tests to the demo application. #68 --- README.md | 12 +- demo_app/.gitignore | 1 + demo_app/lib/main.dart | 25 +- demo_app/lib/repository/todoRepository.dart | 8 +- demo_app/lib/services/todoService.dart | 6 +- demo_app/pubspec.yaml | 2 + demo_app/test/unit/todoRepository_test.dart | 44 +++ .../test/unit/todoRepository_test.mocks.dart | 263 ++++++++++++++++++ demo_app/test/unit/todoService_test.dart | 53 ++++ .../test/unit/todoService_test.mocks.dart | 263 ++++++++++++++++++ demo_app/test/widget/widget_test.dart | 163 +++++++++++ demo_app/test/widget/widget_test.mocks.dart | 67 +++++ demo_app/test/widget_test.dart | 30 -- 13 files changed, 883 insertions(+), 54 deletions(-) create mode 100644 demo_app/test/unit/todoRepository_test.dart create mode 100644 demo_app/test/unit/todoRepository_test.mocks.dart create mode 100644 demo_app/test/unit/todoService_test.dart create mode 100644 demo_app/test/unit/todoService_test.mocks.dart create mode 100644 demo_app/test/widget/widget_test.dart create mode 100644 demo_app/test/widget/widget_test.mocks.dart delete mode 100644 demo_app/test/widget_test.dart diff --git a/README.md b/README.md index c5f24a9..9c61e61 100644 --- a/README.md +++ b/README.md @@ -2076,7 +2076,7 @@ class _TodoListState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Todo item List'), + title: const Text('todo item list'), ), body: FutureBuilder>( future: futureTodosList, @@ -2132,7 +2132,7 @@ class _TodoListState extends State { Nothing was fundamentally changed. We wrapped the `TodoList` with the same widgets of the `MyHomePage` widget. We also changed the `AppBar.title` -to `Text('Todo item list')`. +to `Text('todo item list')`. We now also need to change the `MyApp` to call this newly edited widget. @@ -2163,12 +2163,12 @@ to navigate to the new page. Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Todo item List'), + title: const Text('todo item list'), actions: [ IconButton( icon: const Icon(Icons.list), onPressed: _pushCompleted, - tooltip: 'Completed todo list', + tooltip: 'completed todo list', ), ], ), @@ -2203,7 +2203,7 @@ Add the following function in `_TodoListState`. return Scaffold( appBar: AppBar( - title: const Text('Completed todo list'), + title: const Text('completed todo list'), ), body: ListView(children: divided), ); @@ -2266,6 +2266,6 @@ at this repo's `guides` folder to learn about [logging in with Firebase](./guides/login-firebase-tutorial.md) or [webviews in Flutter](./guides/webview-tutorial.md) -If you want to see more examples, check these out! +If you want to see more fully tested, check these out! - [flutter-todo-list-tutorial](https://github.com/dwyl/flutter-todo-list-tutorial) - [flutter-counter-example](https://github.com/dwyl/flutter-counter-example) \ No newline at end of file diff --git a/demo_app/.gitignore b/demo_app/.gitignore index 24476c5..00e0e8c 100644 --- a/demo_app/.gitignore +++ b/demo_app/.gitignore @@ -9,6 +9,7 @@ .history .svn/ migrate_working_dir/ +coverage # IntelliJ related *.iml diff --git a/demo_app/lib/main.dart b/demo_app/lib/main.dart index df27fdd..dcb0aac 100644 --- a/demo_app/lib/main.dart +++ b/demo_app/lib/main.dart @@ -3,9 +3,11 @@ import 'package:flutter/material.dart'; import 'services/todoService.dart'; +// coverage:ignore-start void main() { runApp(const MyApp()); } +// coverage:ignore-end class MyApp extends StatelessWidget { const MyApp({super.key}); @@ -15,18 +17,19 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Todo App', theme: ThemeData( - appBarTheme: const AppBarTheme( - backgroundColor: Colors.black, - foregroundColor: Colors.white, - ) - ), - home: const TodoList(), + appBarTheme: const AppBarTheme( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + )), + home: TodoList(todoService: TodoService()), ); } } class TodoList extends StatefulWidget { - const TodoList({super.key}); + final TodoService todoService; + + const TodoList({super.key, required this.todoService}); @override State createState() => _TodoListState(); @@ -59,7 +62,7 @@ class _TodoListState extends State { return Scaffold( appBar: AppBar( - title: const Text('Completed todo list'), + title: const Text('completed todo list'), ), body: ListView(children: divided), ); @@ -71,19 +74,19 @@ class _TodoListState extends State { @override void initState() { super.initState(); - futureTodosList = TodoService().getTodos(); + futureTodosList = widget.todoService.getTodos(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Todo item List'), + title: const Text('todo item list'), actions: [ IconButton( icon: const Icon(Icons.list), onPressed: _pushCompleted, - tooltip: 'Completed todo list', + tooltip: 'completed todo list', ), ], ), diff --git a/demo_app/lib/repository/todoRepository.dart b/demo_app/lib/repository/todoRepository.dart index a8a8c66..3fb6421 100644 --- a/demo_app/lib/repository/todoRepository.dart +++ b/demo_app/lib/repository/todoRepository.dart @@ -1,17 +1,21 @@ import 'dart:convert'; import 'package:demo_app/models/todo.dart'; +import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:http/http.dart' show Client; abstract class TodoRepository { Future> getTodos(); } class HTTPTodoRepository implements TodoRepository { + Client client = Client(); + @override Future> getTodos() async { - final response = - await http.get(Uri.parse("https://jsonplaceholder.typicode.com/todos")); + final response = await client + .get(Uri.parse("https://jsonplaceholder.typicode.com/todos")); if (response.statusCode == 200) { Iterable l = json.decode(response.body); diff --git a/demo_app/lib/services/todoService.dart b/demo_app/lib/services/todoService.dart index 3f00e65..7f2f80e 100644 --- a/demo_app/lib/services/todoService.dart +++ b/demo_app/lib/services/todoService.dart @@ -2,11 +2,7 @@ import 'package:demo_app/models/todo.dart'; import 'package:demo_app/repository/todoRepository.dart'; class TodoService { - late final TodoRepository todoRepository; - - TodoService() { - todoRepository = HTTPTodoRepository(); - } + TodoRepository todoRepository = HTTPTodoRepository(); Future> getTodos() => todoRepository.getTodos(); } diff --git a/demo_app/pubspec.yaml b/demo_app/pubspec.yaml index 476d762..0a4d0fd 100644 --- a/demo_app/pubspec.yaml +++ b/demo_app/pubspec.yaml @@ -48,6 +48,8 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^2.0.0 + mockito: 5.3.2 + build_runner: 2.3.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/demo_app/test/unit/todoRepository_test.dart b/demo_app/test/unit/todoRepository_test.dart new file mode 100644 index 0000000..0d7d3f6 --- /dev/null +++ b/demo_app/test/unit/todoRepository_test.dart @@ -0,0 +1,44 @@ +import 'package:demo_app/models/todo.dart'; +import 'package:demo_app/repository/todoRepository.dart'; +import 'package:http/http.dart' as http; +import 'package:mockito/annotations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import 'todoRepository_test.mocks.dart'; + +// Generate a MockClient using the Mockito package. +// Create new instances of this class in each test. +@GenerateMocks([http.Client]) +void main() { + test('Checks if a Todo array is yielded and has the expected length', + () async { + final client = MockClient(); + final repo = HTTPTodoRepository(); + + // Use Mockito to return a successful response when it calls the + // provided http.Client. + when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/todos'))) + .thenAnswer((_) async => http.Response( + '[{"userId": 1, "id": 2, "title": "mock", "completed": true}]', + 200)); + + repo.client = client; + + expect(await repo.getTodos().then((value) => value.length), equals(1)); + }); + + test('throws an exception if the http call completes with an error', () { + final client = MockClient(); + final repo = HTTPTodoRepository(); + + // Use Mockito to return an unsuccessful response when it calls the + // provided http.Client. + when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/todos'))) + .thenAnswer((_) async => http.Response('Not Found', 404)); + + repo.client = client; + + expect(repo.getTodos(), throwsException); + }); +} diff --git a/demo_app/test/unit/todoRepository_test.mocks.dart b/demo_app/test/unit/todoRepository_test.mocks.dart new file mode 100644 index 0000000..2fe405d --- /dev/null +++ b/demo_app/test/unit/todoRepository_test.mocks.dart @@ -0,0 +1,263 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in demo_app/test/unit/todoRepository_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; +import 'dart:convert' as _i4; +import 'dart:typed_data' as _i5; + +import 'package:http/http.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeStreamedResponse_1 extends _i1.SmartFake + implements _i2.StreamedResponse { + _FakeStreamedResponse_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [Client]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockClient extends _i1.Mock implements _i2.Client { + MockClient() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.Response> head( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future<_i2.Response>); + @override + _i3.Future<_i2.Response> get( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future<_i2.Response>); + @override + _i3.Future<_i2.Response> post( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + @override + _i3.Future<_i2.Response> put( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + @override + _i3.Future<_i2.Response> patch( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + @override + _i3.Future<_i2.Response> delete( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + @override + _i3.Future read( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future.value(''), + ) as _i3.Future); + @override + _i3.Future<_i5.Uint8List> readBytes( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #readBytes, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), + ) as _i3.Future<_i5.Uint8List>); + @override + _i3.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => + (super.noSuchMethod( + Invocation.method( + #send, + [request], + ), + returnValue: + _i3.Future<_i2.StreamedResponse>.value(_FakeStreamedResponse_1( + this, + Invocation.method( + #send, + [request], + ), + )), + ) as _i3.Future<_i2.StreamedResponse>); + @override + void close() => super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/demo_app/test/unit/todoService_test.dart b/demo_app/test/unit/todoService_test.dart new file mode 100644 index 0000000..be46400 --- /dev/null +++ b/demo_app/test/unit/todoService_test.dart @@ -0,0 +1,53 @@ +import 'package:demo_app/models/todo.dart'; +import 'package:demo_app/repository/todoRepository.dart'; +import 'package:demo_app/services/todoService.dart'; +import 'package:http/http.dart' as http; +import 'package:mockito/annotations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:flutter/foundation.dart'; + +import 'todoService_test.mocks.dart'; + +// Generate a MockClient using the Mockito package. +// Create new instances of this class in each test. +@GenerateMocks([http.Client]) +void main() { + test( + 'Checks if a Todo array is returned from the service and has the expected length', + () async { + final client = MockClient(); + final repo = HTTPTodoRepository(); + + // Use Mockito to return a successful response when it calls the + // provided http.Client. + when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/todos'))) + .thenAnswer((_) async => http.Response( + '[{"userId": 1, "id": 2, "title": "mock", "completed": true}]', + 200)); + + repo.client = client; + + final service = TodoService(); + service.todoRepository = repo; + + expect(await service.getTodos().then((value) => value.length), equals(1)); + }); + + test('throws an exception if the http call completes with an error', () { + final client = MockClient(); + final repo = HTTPTodoRepository(); + + // Use Mockito to return an unsuccessful response when it calls the + // provided http.Client. + when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/todos'))) + .thenAnswer((_) async => http.Response('Not Found', 404)); + + repo.client = client; + + final service = TodoService(); + service.todoRepository = repo; + + expect(service.getTodos(), throwsException); + }); +} diff --git a/demo_app/test/unit/todoService_test.mocks.dart b/demo_app/test/unit/todoService_test.mocks.dart new file mode 100644 index 0000000..2e72242 --- /dev/null +++ b/demo_app/test/unit/todoService_test.mocks.dart @@ -0,0 +1,263 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in demo_app/test/unit/todoService_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; +import 'dart:convert' as _i4; +import 'dart:typed_data' as _i5; + +import 'package:http/http.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeStreamedResponse_1 extends _i1.SmartFake + implements _i2.StreamedResponse { + _FakeStreamedResponse_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [Client]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockClient extends _i1.Mock implements _i2.Client { + MockClient() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.Response> head( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future<_i2.Response>); + @override + _i3.Future<_i2.Response> get( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future<_i2.Response>); + @override + _i3.Future<_i2.Response> post( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + @override + _i3.Future<_i2.Response> put( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + @override + _i3.Future<_i2.Response> patch( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + @override + _i3.Future<_i2.Response> delete( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + @override + _i3.Future read( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future.value(''), + ) as _i3.Future); + @override + _i3.Future<_i5.Uint8List> readBytes( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #readBytes, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), + ) as _i3.Future<_i5.Uint8List>); + @override + _i3.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => + (super.noSuchMethod( + Invocation.method( + #send, + [request], + ), + returnValue: + _i3.Future<_i2.StreamedResponse>.value(_FakeStreamedResponse_1( + this, + Invocation.method( + #send, + [request], + ), + )), + ) as _i3.Future<_i2.StreamedResponse>); + @override + void close() => super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/demo_app/test/widget/widget_test.dart b/demo_app/test/widget/widget_test.dart new file mode 100644 index 0000000..6fc0e59 --- /dev/null +++ b/demo_app/test/widget/widget_test.dart @@ -0,0 +1,163 @@ +import 'package:demo_app/models/todo.dart'; +import 'package:demo_app/services/todoService.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; + +import 'package:demo_app/main.dart'; + +import 'widget_test.mocks.dart'; + +@GenerateMocks([TodoService]) +void main() { + testWidgets('Check if appbar renders', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that the appbar renders + expect(find.text('todo item list'), findsOneWidget); + }); + + testWidgets('Check if item list is rendered', (WidgetTester tester) async { + final TodoService mockService = MockTodoService(); + + when(mockService.getTodos()).thenAnswer((_) async => + [const Todo(userId: 1, id: 1, title: 'mocktitle', completed: true)]); + + Widget testWidget = MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: TodoList(todoService: mockService))); + + await tester.pumpWidget(testWidget); + await tester.pump(const Duration(milliseconds: 100)); + + // Expect the mocked todo item to be displayed + expect(find.text('mocktitle'), findsOneWidget); + }); + + testWidgets('Error should be displayed if the server returns error', + (WidgetTester tester) async { + final TodoService mockService = MockTodoService(); + + when(mockService.getTodos()) + .thenAnswer((_) async => throw Exception('Error getting todos.')); + + Widget testWidget = MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: TodoList(todoService: mockService))); + + await tester.pumpWidget(testWidget); + await tester.pump(const Duration(milliseconds: 100)); + + // Expect the mocked todo item to be displayed + expect(find.text('Exception: Error getting todos.'), findsOneWidget); + }); + + testWidgets('Tapping on a todo item and navigating to the done list page.', + (WidgetTester tester) async { + final TodoService mockService = MockTodoService(); + + when(mockService.getTodos()).thenAnswer((_) async => [ + const Todo(userId: 1, id: 1, title: 'mocktitle', completed: true), + const Todo(userId: 1, id: 2, title: 'mocktitle2', completed: true), + ]); + + Widget testWidget = MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: TodoList(todoService: mockService))); + + await tester.pumpWidget(testWidget); + await tester.pump(const Duration(milliseconds: 100)); + + // Tapping on a todo + await tester.tap(find.text('mocktitle')); + await tester.pumpAndSettle(); + + // Navigating away + await tester.tap(find.byIcon((Icons.list))); + await tester.pumpAndSettle(); + + // Expect the todo list page to be shown + expect(find.text('completed todo list'), findsOneWidget); + expect(find.text('todo item list'), findsNothing); + }); + + testWidgets('Navigating to the todo list directly and find empty widget array', + (WidgetTester tester) async { + final TodoService mockService = MockTodoService(); + + when(mockService.getTodos()).thenAnswer((_) async => [ + const Todo(userId: 1, id: 1, title: 'mocktitle', completed: true), + const Todo(userId: 1, id: 2, title: 'mocktitle2', completed: true), + ]); + + Widget testWidget = MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: TodoList(todoService: mockService))); + + await tester.pumpWidget(testWidget); + await tester.pump(const Duration(milliseconds: 100)); + + // Navigating away + await tester.tap(find.byIcon((Icons.list))); + await tester.pumpAndSettle(); + + // Expect the todo list page to be shown + expect(find.text('completed todo list'), findsOneWidget); + }); + + testWidgets('Marking todo as done and then as undone', + (WidgetTester tester) async { + final TodoService mockService = MockTodoService(); + + when(mockService.getTodos()).thenAnswer((_) async => [ + const Todo(userId: 1, id: 1, title: 'mocktitle', completed: true), + const Todo(userId: 1, id: 2, title: 'mocktitle2', completed: true), + ]); + + Widget testWidget = MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: TodoList(todoService: mockService))); + + await tester.pumpWidget(testWidget); + await tester.pump(const Duration(milliseconds: 100)); + + // Tap and untap + await tester.tap(find.text('mocktitle')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('mocktitle')); + await tester.pumpAndSettle(); + + // Expect the todo list page to be shown + expect(find.text('mocktitle'), findsOneWidget); + }); + + testWidgets('Testing main mount', + (WidgetTester tester) async { + final TodoService mockService = MockTodoService(); + + when(mockService.getTodos()).thenAnswer((_) async => [ + const Todo(userId: 1, id: 1, title: 'mocktitle', completed: true), + const Todo(userId: 1, id: 2, title: 'mocktitle2', completed: true), + ]); + + Widget testWidget = MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: TodoList(todoService: mockService))); + + await tester.pumpWidget(testWidget); + await tester.pump(const Duration(milliseconds: 100)); + + // Tap and untap + await tester.tap(find.text('mocktitle')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('mocktitle')); + await tester.pumpAndSettle(); + + // Expect the todo list page to be shown + expect(find.text('mocktitle'), findsOneWidget); + }); +} diff --git a/demo_app/test/widget/widget_test.mocks.dart b/demo_app/test/widget/widget_test.mocks.dart new file mode 100644 index 0000000..aed4c89 --- /dev/null +++ b/demo_app/test/widget/widget_test.mocks.dart @@ -0,0 +1,67 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in demo_app/test/widget_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i4; + +import 'package:demo_app/models/todo.dart' as _i5; +import 'package:demo_app/repository/todoRepository.dart' as _i2; +import 'package:demo_app/services/todoService.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeTodoRepository_0 extends _i1.SmartFake + implements _i2.TodoRepository { + _FakeTodoRepository_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [TodoService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTodoService extends _i1.Mock implements _i3.TodoService { + MockTodoService() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.TodoRepository get todoRepository => (super.noSuchMethod( + Invocation.getter(#todoRepository), + returnValue: _FakeTodoRepository_0( + this, + Invocation.getter(#todoRepository), + ), + ) as _i2.TodoRepository); + @override + set todoRepository(_i2.TodoRepository? _todoRepository) => super.noSuchMethod( + Invocation.setter( + #todoRepository, + _todoRepository, + ), + returnValueForMissingStub: null, + ); + @override + _i4.Future> getTodos() => (super.noSuchMethod( + Invocation.method( + #getTodos, + [], + ), + returnValue: _i4.Future>.value(<_i5.Todo>[]), + ) as _i4.Future>); +} diff --git a/demo_app/test/widget_test.dart b/demo_app/test/widget_test.dart deleted file mode 100644 index 40adf93..0000000 --- a/demo_app/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// 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:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:demo_app/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} From e0263789554993f1451559ad879687b46010e454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Thu, 10 Nov 2022 16:01:39 +0000 Subject: [PATCH 24/33] feat: Adding testing guide. #68 --- README.md | 716 +++++++++++++++++++- demo_app/README.md | 16 - demo_app/lib/repository/todoRepository.dart | 2 - demo_app/test/unit/todoService_test.dart | 2 - 4 files changed, 714 insertions(+), 22 deletions(-) delete mode 100644 demo_app/README.md diff --git a/README.md b/README.md index 9c61e61..6a8d4d9 100644 --- a/README.md +++ b/README.md @@ -1122,6 +1122,71 @@ shared app state is not a beginner-friendly topic to learn and is often very opinionated. As long as you understood *what it is*, it's awesome! :tada: +### Dependency injection +You might be wondering what dependency injection +has to do with the aforementioned state management libraries. +You'll see why this effects how the code is structure and +how it effects testing. + +> "[Dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) +> is a design pattern +> in which an object or function +> receives other objects or functions that it depends on." + +Let's write an example of dependency injection in Flutter +in its simplest form. + +```dart +class LoginService { + Api api; + + // Inject the API through the constructor + LoginService(this.api) +} + +class Api {} +``` + +Here, the `LoginService` receives the `Api` object +in the constructor, something it depends on. +This is no problem if the `LoginService` is one or +two levels deep from a widget it uses it. +However, it does become a problem when it's +ten levels deep. + +```sh +Widget 1 -> Widget 2 -> Widget 3 -> Widget 4 +``` + +Let's consider we have a `Widget X`, that returns a list of albums. +If `Widget 4` needed these list of albums, it would need `Widget X`. +To do this, `Widget X` would need to be passed on +from `Widget 1` all the way to `Widget 4` so `Widget 4` could +use it. This is not sustainable and it can become nightmarish. + +Instead of using a [singleton](https://en.wikipedia.org/wiki/Singleton_pattern) +which can often lead to unexpected behaviour and +harder to test codebase, we need to use *dependency injection*. +But in cases of deeply nested widgets, using packages like +[`get_it`](https://pub.dev/packages/get_it) or +[`Riverpod`](https://riverpod.dev/) or +[`Provider`](https://pub.dev/packages/provider) +are the way to go, as they give us much better +control over our dependencies without any of the +drawbacks of creating our own singletons with `Singleton.instance`, +allowing us to inject dependencies and accessin values +in deeply nested widgets without chaining dependencies +along the widget tree. +This is also useful for mocking objects in testing. + +If you are interested in how you would +implement these, we highly recommend taking a look +at this video -> https://www.youtube.com/watch?v=vBT-FhgMaWM&ab_channel=FilledStacks . +It's a 10 minute video that explains this topic in +simple terms and shows implementation examples using +`get_it` and `Provider`. Great stuff! + + # Testing πŸ§ͺ @@ -1371,6 +1436,16 @@ test how all of these *work together*, as a whole. These tasks are captured and tested with **integration tests**. +> There is a concept in Flutter that is **widget testing**. +> Widget testing tests a single widget while +> integration testing can test a complete app or large parts of it. +> Integration testing will require *a device* or *emulator*. +> So it should be used sparingly and to capture behaviours +> that were missed by unit testing and widget testing. +> +> Implementation-wise, widget testing uses the +> `testWidget` function, much like integration tests. So they can be similar. + We can luckily leverage the SDK's [`integration_test`](https://github.com/flutter/flutter/tree/main/packages/integration_test) package to do this. @@ -2039,7 +2114,7 @@ and set them as `done` and reverse that action. Great job! ![interactivity](https://user-images.githubusercontent.com/17494745/200861445-b4550a49-98cc-4f80-ba02-6ceff7fa17da.gif) -# 4. Adding navigation +## 4. Adding navigation We have added a stateful widget and are keeping track of what todos are marked as `completed` or not. It would be great to actually have a page where we see this list of completed items. @@ -2229,7 +2304,7 @@ If we rerun our app, we can now navigate between pages. Hurray! :tada: ![navigation](https://user-images.githubusercontent.com/17494745/200880357-314bb388-5c0c-4955-ac22-f9ec59e418a6.gif) -# 5. Finishing touches +## 5. Finishing touches We can quickly custmize the theme of the app, and it's title. Let's change the colors and give our fancy app a new title. @@ -2254,6 +2329,643 @@ You can choose the colors you like. Go creative! :tada: final +## 6. Testing! +We have our app running. In fact, we should +have used a [TDD](https://github.com/dwyl/learn-tdd) +approach to get our app running. +The reason we didn't do this is to show you how some +code needs to be laid out to be *testable*. + +As you previously seen, mocking objects in Flutter +works through **dependency injection**. +That is, these are functions receive the dependencies that +they depend on through, for example, their constructor. + +For simplicity sake, we are not going to be using +any libraries like `get_it` or `Riverpod` to do +deeply nested dependency injection. +In our demo app, we only have two levels deep, +so mocking and testing is very simple. + +Let's start testing! + +### 6.1 Unit testing +Let's start unit testing our `TodoRepository` +and `TodoService`. As it stands, both of these files +are not "testable". We ought to find a way to +mock the `http` requests. How do we do that? +Exactly. *Dependency injection*. + +But first, we need to add the dependencies +in `pubspec.yaml`. +In the `dev_dependencies` section, +add the following two lines of code. + +```yaml + mockito: 5.3.2 + build_runner: 2.3.2 +``` + +And run `flutter pub get`. +This will download the newly added dependencies. +Now let's start testing! + +Change `lib/repository/todoRepository.dart` +so it looks like the following. + +```dart +import 'package:http/http.dart' show Client; + +class HTTPTodoRepository implements TodoRepository { + Client client = Client(); + + @override + Future> getTodos() async { + final response = await client + .get(Uri.parse("https://jsonplaceholder.typicode.com/todos")); + + if (response.statusCode == 200) { + Iterable l = json.decode(response.body); + List todos = + List.from(l.map((model) => Todo.fromJson(model))); + + return todos; + } else { + throw Exception('Failed to load Todo\'s.'); + } + } +} +``` + +Now, on to testing. Create a directory in `test/unit` +and add a new file `todoRepository_test.dart`. + +```dart +import 'package:http/http.dart' as http; +import 'package:mockito/annotations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +@GenerateMocks([http.Client]) +void main() { +} +``` + +We are going to use `mockito`'s `@GenerateMocks` annotation +to generate a mock object for the `http.Client`, +which is used inside the function. +We could do it manually but since we can get it generated +to ourselves automatically, let's do it. + +Run the following command. + +```sh +flutter pub run build_runner build --delete-conflicting-outputs +``` + +This will generate a `todoRepository_test.mocks.dart` file +with the generated mocks. +Import the file in the `todoRepository_test.dart` file and +let's create our first tests! + +```dart +import 'todoRepository_test.mocks.dart'; + +@GenerateMocks([http.Client]) +void main() { + test('Checks if a Todo array is yielded and has the expected length', + () async { + final client = MockClient(); + final repo = HTTPTodoRepository(); + + // Use Mockito to return a successful response when it calls the + // provided http.Client. + when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/todos'))) + .thenAnswer((_) async => http.Response( + '[{"userId": 1, "id": 2, "title": "mock", "completed": true}]', + 200)); + + repo.client = client; + + expect(await repo.getTodos().then((value) => value.length), equals(1)); + }); + + test('throws an exception if the http call completes with an error', () { + final client = MockClient(); + final repo = HTTPTodoRepository(); + + // Use Mockito to return an unsuccessful response when it calls the + // provided http.Client. + when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/todos'))) + .thenAnswer((_) async => http.Response('Not Found', 404)); + + repo.client = client; + + expect(repo.getTodos(), throwsException); + }); +} +``` + +Let's break down how we are testing the repository. +We are creating a `final client = MockClient()` using +the generated `MockClient` from the `todoRepository_test.mocks.dart` +file. We are specifying that this client +will return an array with a single todo item. +Using this new `MockClient`, we replace the class `client` +with the `MockClient` and run the test. +The same procedure is done, except an exception +is expected to rise. + +Let's do the same process for the `TodoService.dart` file. +We need to change it, like so. + +```dart +import 'package:demo_app/models/todo.dart'; +import 'package:demo_app/repository/todoRepository.dart'; + +class TodoService { + TodoRepository todoRepository = HTTPTodoRepository(); + + Future> getTodos() => todoRepository.getTodos(); +} +``` + + +Create a new `todoService_test.dart` and add +the following lines of code. + +```dart +import 'package:http/http.dart' as http; +import 'package:mockito/annotations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +@GenerateMocks([http.Client]) +void main() { +} +``` + +Run the following command. + +```sh +flutter pub run build_runner build --delete-conflicting-outputs +``` + +This will generate a `todoService_test.mocks.dart` file +with the generated mocks. Similarly, we will use this +file for the tests in the same fashion as before. +In `todoService_test.dart`, add the following code. + +```dart +import 'todoService_test.mocks.dart'; + +// Generate a MockClient using the Mockito package. +// Create new instances of this class in each test. +@GenerateMocks([http.Client]) +void main() { + test( + 'Checks if a Todo array is returned from the service and has the expected length', + () async { + final client = MockClient(); + final repo = HTTPTodoRepository(); + + // Use Mockito to return a successful response when it calls the + // provided http.Client. + when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/todos'))) + .thenAnswer((_) async => http.Response( + '[{"userId": 1, "id": 2, "title": "mock", "completed": true}]', + 200)); + + repo.client = client; + + final service = TodoService(); + service.todoRepository = repo; + + expect(await service.getTodos().then((value) => value.length), equals(1)); + }); + + test('throws an exception if the http call completes with an error', () { + final client = MockClient(); + final repo = HTTPTodoRepository(); + + // Use Mockito to return an unsuccessful response when it calls the + // provided http.Client. + when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/todos'))) + .thenAnswer((_) async => http.Response('Not Found', 404)); + + repo.client = client; + + final service = TodoService(); + service.todoRepository = repo; + + expect(service.getTodos(), throwsException); + }); +} + +``` + +This is the same process as before. +We have all the unit tests we want. +Let's run the following command. + +```sh +flutter test --coverage +``` + +This will run the four tests. +They should all pass. +All that's left is testing the widgets. +Let's do it! + +### 6.2 Widget testing +To test our widgets, we need to pass +the `TodoService` so we can mock it in our tests. +Normally we would use a Provider to do this but this +is a simple app, so there is no need to add complexity +and third-party libraries. + +Let's do these changes. Head over to `lib/main.dart` +and change the `TodoList` class like so. + +```dart +class TodoList extends StatefulWidget { + final TodoService todoService; + + const TodoList({super.key, required this.todoService}); + + @override + State createState() => _TodoListState(); +} +``` + +Now, we need to change the `MyApp` class to pass +a `TodoService` instance to `TodoList`. +It should look like this, now. + +```dart +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Todo App', + theme: ThemeData( + appBarTheme: const AppBarTheme( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + )), + home: TodoList(todoService: TodoService()), + ); + } +} +``` + +Now we can test these widgets! +Create a new directory `test/widget` and +create a file named `widget_test.dart`. + +```dart + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; +import 'package:demo_app/main.dart'; + + +@GenerateMocks([TodoService]) +void main() { +} +``` + +Run the following command. + +```sh +flutter pub run build_runner build --delete-conflicting-outputs +``` + +This will generate a `widget_test.mocks.dart` file +with the generated mocks for `TodoService`. +Now we are ready to test our first widget! +Add the following test inside `main()`. + +```dart +import 'widget_test.mocks.dart'; + +@GenerateMocks([TodoService]) +void main() { + testWidgets('Check if appbar renders', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that the appbar renders + expect(find.text('todo item list'), findsOneWidget); + }); +} +``` + +We use the `testWidgets` function to test the widget. +In turn, we get a `tester` object which allows us +to perform actions. We initialize and create the +widget by using `await test.pumpWidget(const MyApp())`. +We then check if the app bar is rendered. +To do this, we use the `find` class to find +the widget by text and check if it was built in the widget tree. +We then use a `Matcher` to make the assertion. +In this case, we check if we `findOneWidget`. + +If we run `flutter test --coverage`, we will see this test should pass. + +Let's now add a test to check if the list is rendered +with a list of todos. Add the following test. + +```dart + testWidgets('Check if item list is rendered', (WidgetTester tester) async { + final TodoService mockService = MockTodoService(); + + when(mockService.getTodos()).thenAnswer((_) async => + [const Todo(userId: 1, id: 1, title: 'mocktitle', completed: true)]); + + Widget testWidget = MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: TodoList(todoService: mockService))); + + await tester.pumpWidget(testWidget); + await tester.pump(const Duration(milliseconds: 100)); + + // Expect the mocked todo item to be displayed + expect(find.text('mocktitle'), findsOneWidget); + }); +``` + +In this test, we are instantiating a `MockTodoService`, +specifying the return value of the `getTodos()` +and then using it when creating a `TodoList` widget. +We can't create `TodoList` by itself because +it necessitates to be a child of `MaterialApp`. +Hence why we use `MediaQuery` with `MaterialApp` which in turn +creates a `TodoList` widget that we want to test. + +With `tester.pumpWidget()`, we instantiate the +widget. This won't suffice, though. +The widget needs to render any animations and +run `initState` to fetch the todos item. +For this, we add `await tester.pump()` with a specified duration. +This schedules a frame and triggers a rebuild of the widget, +running the clock by that amount. +We only need `100 ms` in our case. + +After this, we assert if the rendered list +contains a todo item with a title "mocktitle". + +Let's add another test. + +```dart + testWidgets('Navigating to the todo list directly and find empty widget array', + (WidgetTester tester) async { + final TodoService mockService = MockTodoService(); + + when(mockService.getTodos()).thenAnswer((_) async => [ + const Todo(userId: 1, id: 1, title: 'mocktitle', completed: true), + const Todo(userId: 1, id: 2, title: 'mocktitle2', completed: true), + ]); + + Widget testWidget = MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: TodoList(todoService: mockService))); + + await tester.pumpWidget(testWidget); + await tester.pump(const Duration(milliseconds: 100)); + + // Navigating away + await tester.tap(find.byIcon((Icons.list))); + await tester.pumpAndSettle(); + + // Expect the todo list page to be shown + expect(find.text('completed todo list'), findsOneWidget); + }); +``` + +In this test, we are rendering the `TodoList`, +tapping on a todo item (thus marking it as `complete`) +and then navigating to the done todo item list. +For this, we use `tester.tap(find.byIcon((Icons.list)))` +to find the button and tap it. +We then use `tester.pumpAndSettle()`, which essentially +waits for all the animations to complete. +After this, we check if the done list screen is rendered +in the widget tree. + +We can keep adding tests to cover the rest of +the scenarios. Copy the following code and replace +your existing tests to cover all edge cases. +Your `main` function should now look like this. + +```dart +@GenerateMocks([TodoService]) +void main() { + testWidgets('Check if appbar renders', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that the appbar renders + expect(find.text('todo item list'), findsOneWidget); + }); + + testWidgets('Check if item list is rendered', (WidgetTester tester) async { + final TodoService mockService = MockTodoService(); + + when(mockService.getTodos()).thenAnswer((_) async => + [const Todo(userId: 1, id: 1, title: 'mocktitle', completed: true)]); + + Widget testWidget = MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: TodoList(todoService: mockService))); + + await tester.pumpWidget(testWidget); + await tester.pump(const Duration(milliseconds: 100)); + + // Expect the mocked todo item to be displayed + expect(find.text('mocktitle'), findsOneWidget); + }); + + testWidgets('Error should be displayed if the server returns error', + (WidgetTester tester) async { + final TodoService mockService = MockTodoService(); + + when(mockService.getTodos()) + .thenAnswer((_) async => throw Exception('Error getting todos.')); + + Widget testWidget = MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: TodoList(todoService: mockService))); + + await tester.pumpWidget(testWidget); + await tester.pump(const Duration(milliseconds: 100)); + + // Expect the mocked todo item to be displayed + expect(find.text('Exception: Error getting todos.'), findsOneWidget); + }); + + testWidgets('Tapping on a todo item and navigating to the done list page.', + (WidgetTester tester) async { + final TodoService mockService = MockTodoService(); + + when(mockService.getTodos()).thenAnswer((_) async => [ + const Todo(userId: 1, id: 1, title: 'mocktitle', completed: true), + const Todo(userId: 1, id: 2, title: 'mocktitle2', completed: true), + ]); + + Widget testWidget = MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: TodoList(todoService: mockService))); + + await tester.pumpWidget(testWidget); + await tester.pump(const Duration(milliseconds: 100)); + + // Tapping on a todo + await tester.tap(find.text('mocktitle')); + await tester.pumpAndSettle(); + + // Navigating away + await tester.tap(find.byIcon((Icons.list))); + await tester.pumpAndSettle(); + + // Expect the todo list page to be shown + expect(find.text('completed todo list'), findsOneWidget); + expect(find.text('todo item list'), findsNothing); + }); + + testWidgets('Navigating to the todo list directly and find empty widget array', + (WidgetTester tester) async { + final TodoService mockService = MockTodoService(); + + when(mockService.getTodos()).thenAnswer((_) async => [ + const Todo(userId: 1, id: 1, title: 'mocktitle', completed: true), + const Todo(userId: 1, id: 2, title: 'mocktitle2', completed: true), + ]); + + Widget testWidget = MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: TodoList(todoService: mockService))); + + await tester.pumpWidget(testWidget); + await tester.pump(const Duration(milliseconds: 100)); + + // Navigating away + await tester.tap(find.byIcon((Icons.list))); + await tester.pumpAndSettle(); + + // Expect the todo list page to be shown + expect(find.text('completed todo list'), findsOneWidget); + }); + + testWidgets('Marking todo as done and then as undone', + (WidgetTester tester) async { + final TodoService mockService = MockTodoService(); + + when(mockService.getTodos()).thenAnswer((_) async => [ + const Todo(userId: 1, id: 1, title: 'mocktitle', completed: true), + const Todo(userId: 1, id: 2, title: 'mocktitle2', completed: true), + ]); + + Widget testWidget = MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: TodoList(todoService: mockService))); + + await tester.pumpWidget(testWidget); + await tester.pump(const Duration(milliseconds: 100)); + + // Tap and untap + await tester.tap(find.text('mocktitle')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('mocktitle')); + await tester.pumpAndSettle(); + + // Expect the todo list page to be shown + expect(find.text('mocktitle'), findsOneWidget); + }); + + testWidgets('Testing main mount', + (WidgetTester tester) async { + final TodoService mockService = MockTodoService(); + + when(mockService.getTodos()).thenAnswer((_) async => [ + const Todo(userId: 1, id: 1, title: 'mocktitle', completed: true), + const Todo(userId: 1, id: 2, title: 'mocktitle2', completed: true), + ]); + + Widget testWidget = MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: TodoList(todoService: mockService))); + + await tester.pumpWidget(testWidget); + await tester.pump(const Duration(milliseconds: 100)); + + // Tap and untap + await tester.tap(find.text('mocktitle')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('mocktitle')); + await tester.pumpAndSettle(); + + // Expect the todo list page to be shown + expect(find.text('mocktitle'), findsOneWidget); + }); +} +``` + +The final changes we ought to do is in the `main.dart` file. +We can't directly test the `main()` function that +runs the application. +So, in order to get a real coverage report, +add the following lines around the function. +This way, when testing, the compiler skips this function, +as it is not needed to be tested. + +```dart +// coverage:ignore-start +void main() { + runApp(const MyApp()); +} +// coverage:ignore-end +``` + +### 6.3 Test coverage +To get the test coverage, we are going to simply run +three commands. However, firstly, if you are on MacOS, +you need to install `lcov`. For this, run the following command +to install it in your computer. + +```sh +brew install lcov +``` + +Now, to get the coverage, run the following commands. + +```sh +# Generate `coverage/lcov.info` file +flutter test --coverage +# Generate HTML report +genhtml coverage/lcov.info -o coverage/html +# Open the report +open coverage/html/index.html +``` + +The generated HTML will create files inside +the `coverage/` folder. Add it to your +`.gitignore` file. + +Your browser should have opened a window, +like so. + +image + +Congratulations, you now have a fully tested +application! Awesome job! :tada: + # Final remarks πŸ‘‹ In this document (if you actually read it all the way through πŸ˜‰), diff --git a/demo_app/README.md b/demo_app/README.md deleted file mode 100644 index 1dde134..0000000 --- a/demo_app/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# demo_app - -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/demo_app/lib/repository/todoRepository.dart b/demo_app/lib/repository/todoRepository.dart index 3fb6421..5ed24d0 100644 --- a/demo_app/lib/repository/todoRepository.dart +++ b/demo_app/lib/repository/todoRepository.dart @@ -1,8 +1,6 @@ import 'dart:convert'; import 'package:demo_app/models/todo.dart'; -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:http/http.dart' show Client; abstract class TodoRepository { diff --git a/demo_app/test/unit/todoService_test.dart b/demo_app/test/unit/todoService_test.dart index be46400..8b0d99d 100644 --- a/demo_app/test/unit/todoService_test.dart +++ b/demo_app/test/unit/todoService_test.dart @@ -1,11 +1,9 @@ -import 'package:demo_app/models/todo.dart'; import 'package:demo_app/repository/todoRepository.dart'; import 'package:demo_app/services/todoService.dart'; import 'package:http/http.dart' as http; import 'package:mockito/annotations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:flutter/foundation.dart'; import 'todoService_test.mocks.dart'; From e915f8c1507947b7c3f2de66085b4ebaca7f171a Mon Sep 17 00:00:00 2001 From: nelsonic Date: Fri, 18 Nov 2022 10:49:12 +0000 Subject: [PATCH 25/33] =?UTF-8?q?add=20"Mac=20Focussed=3F=20=F0=9F=8D=8F"?= =?UTF-8?q?=20section=20to=20README.md=20#68?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 133 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 107 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 6a8d4d9..82145de 100644 --- a/README.md +++ b/README.md @@ -13,26 +13,29 @@ for creating multi-platform, high-performance applications from a single codebase. It makes it easier for you to build user interfaces that works both on web and mobile devices. -Flutter uses [Dart](https://github.com/dwyl/learn-dart), a -general-purpose programming language created by Google. -If you come from an object-oriented language like `Java`, `C#`, -`Go` or `Javascript/Typescript`, you will feel right at home. +`Flutter` uses +[`Dart`](https://github.com/dwyl/learn-dart), +a general-purpose programming language created by **Google**. +If you come from an object-oriented programming language +like `Java`, `C#`, +`Go` or `Javascript/Typescript`, +you will feel right at home. # Why? 🀷 -- Flutter can be used to build cross platform native applications -(Android, iOS, Desktop and Web) using the same codebase. +1. Flutter can be used to build cross platform **native** applications +(Android, iOS, Desktop and Web) using the _same_ codebase. This significantly simplifies maintenance costs and dev headache when deploying for either Android or iOS devices. -- The Dart programming language used in Flutter +2. The `Dart` programming language used in `Flutter` is object oriented and familiar to most developers. -Flutter benefits immensely by leveraging Dart. +`Flutter` benefits immensely by leveraging `Dart`. Being a language optimized for UI and compiling to ARM & x64 machine code for mobile, desktop and backend, it offers amazing performance benchmarks. -- Development times are significantly faster +3. Development times are significantly faster than other cross-platform frameworks thanks to stateful hot-reloading and excellent virtual device support. @@ -40,36 +43,115 @@ If we close the application, when we open it again we can continue from where we stopped. -- Flutter has a _complete_ design system +4. `Flutter` has a **_complete_ design system** with a library of Material UI widgets included which speeds up the development process. -- It's growing at a fast-pace and being increasingly used -in production worldwide. +5. `Flutter` is the fastest-growing mobile development platform +and is wildly used in production worldwide. ![fast-pace](https://user-images.githubusercontent.com/194400/84572723-e3b04800-ad93-11ea-85e2-19e9693e5a26.png) -- Flutter has overtaken React Native in searchs, -further showcasing the growing trend of Flutter. -Also, Flutter is [probably more performant](https://www.orientsoftware.com/blog/flutter-vs-react-native-performance/) -than React Native in mobile devices. +`Flutter` has overtaken React Native in Google searches, +further showcasing the growing trend of `Flutter`: + +https://trends.google.com/trends/explore?date=today%205-y&q=flutter,react%20native + +![flutter-vs-react-native](https://user-images.githubusercontent.com/194400/202675546-b2bbdd8a-c4fb-4b97-9e7c-1997fdcf0905.png) -![rn](https://user-images.githubusercontent.com/17494745/198244948-29e5d3a5-1b2b-4d1f-a434-d4eee2a5799c.png) # Who? πŸ‘€ + This repo is useful for anyone that is interested in mobile and web app development. -For anyone that hasn't yet touched Flutter, this +For anyone that hasn't yet touched `Flutter`, this repo is a *great* place to start to get your computer ready for Flutter/Dart development, understand the **main concepts** and *guide* you to then create -your very first Flutter app. +your very first `Flutter` app. + + +## Mac Focussed? 🍏 + +While the _installation_ steps below +include Mac-specific steps like `Homebrew` and `XCode`, +this guide can still _easily_ be followed by people +using Linux or Windows as their OS. + +The _reason_ we use **`Mac`** is simple: +it's the _only_ way to ship apps for **`iOS`**. + +Like it or not, **`iPhone`** now has a +**`50%` Market Share in the US**: +https://www.visualcapitalist.com/iphone-majority-us-smartphones/ + +![iphone-americas-top-smartphone](https://user-images.githubusercontent.com/194400/202679987-28743fa1-45c7-455b-a8b8-ca2f29567628.jpg) + +In Europe, **`iPhone`** ownership/use correlates strongly to wealth of the nation; +Monaco and Norway the two countries with the highest GDP/Capita top the table +with +[**`69.91%`**](https://www.reddit.com/r/MapPorn/comments/xx4gp6/percentage_of_iphone_users_in_europe/) +and +[**`68.89%`**](https://gs.statcounter.com/os-market-share/mobile/norway) +respectively. +https://mezha.media/en/2022/10/10/percentage-of-iphone-users-in-different-european-countries/ + +![europe-iphone-market-share](https://user-images.githubusercontent.com/194400/202684011-b58184e6-3501-42f2-ad63-cca14c8e828f.png) + +Worldwide **`iPhone`** a **`~30%` Market Share**: +https://gs.statcounter.com/os-market-share/mobile/worldwide +Mostly because there are _many_ cheap Android devices +that have flooded the market. + +But by _far_ the most important fact/stat to pay attention +from an Native Mobile App development perspective is: + +### "_`iOS` users spend more than double on subscriptions compared to `Android` users_" + +https://www.phonearena.com/news/app-store-users-spend-more-than-double-google-play-users-subscriptions_id138692 + +So ... if you're building a **`SaaS` product**, +you should pour _most_ of your effort into perfecting the UI/UX on **`iPhone`**. + +This is why we use **`Mac`**computers for our **`Flutter`** dev. +So we can run **`XCode`** and test on **`iOS`** devices +and pay our bills. + + + +# Install ⬇️ + +## Mac: Homebrew 🍺 + +The easiest way to install `Flutter` +on a Mac is using [Homebrew](https://brew.sh/). +After installing Homebrew, +you can install `Flutter` +by running the command: + +```sh +brew install --cask flutter +``` + +You should see something similar to: + +```sh +==> Downloading https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_maco +######################################################################## 100.0% +==> Installing Cask flutter +==> Linking Binary 'dart' to '/opt/homebrew/bin/dart' +==> Linking Binary 'flutter' to '/opt/homebrew/bin/flutter' +🍺 flutter was successfully installed! +``` + + + +## Manual Install -# Installing Flutter ⬇️ Installing Flutter might seem like a daunting task. But do not worry, we'll help you get your local environment -running in no time! Since we are targetting web and mobile, +running in no time! Since we are targeting web and mobile, there are a few tools and SDKs we ought to install first. These steps will be oriented to Mac/Unix devices but you should @@ -78,6 +160,7 @@ don't be shy! Reach out to us and [open an issue](https://github.com/dwyl/learn- we'll get back to you as fast as we can! ## Installing Flutter SDK + Head over to https://docs.flutter.dev/get-started/install, select your operating system and follow the instructions. @@ -103,13 +186,11 @@ you should be able to run the command with no problems. terminal window. It checks it all the necessary tools for development for all devices are correctly installed. Let's do just that. -> If you found this procedure convoluted, you can alternatively -install Flutter through [Homebrew](https://brew.sh/). -After installing Homebrew, you can install Flutter by simply running -`brew install --cask flutter`. ## Install XCode -To install XCode, simply open your AppStore, search for 'XCode' + +If you don't already have **`XCode`** installed, +open your **`AppStore`**, search for 'XCode' and press `Install`. It's that easy. image From 0bb200fad6fb3dfb72553428e7c536975193be19 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Fri, 18 Nov 2022 10:50:12 +0000 Subject: [PATCH 26/33] remove .DS_Store --- .DS_Store | Bin 8196 -> 0 bytes .gitignore | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index e203579c5f21bf658bec0a17e113b4f66e4ee457..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHM-EJF26h4!NX1AuzrcK&3D$)vHfYeZu^j`>d>m)#EA;GrVgtT?n-if{HX2#kb z$8D`h7T3@RC=yptUIB4|Dq3CvA#Qj9F1P`StM<&yI&th)EfPXhosnj~voq(Lo&C&F zVq2r4;+#5rASsokDF)GW9CxKVqL*S@qoxDVbRg-SNt&S$b$8^s${mQO(X>VZqd>m` zGIzHi01E5CyYu&Q#LGB_$7!I7c@VX|v>v&T=OFkkMEX!hoX7mCGRucr(WvEJzf$ zpa(45vP;fHtM%6O%+#biedgrM=A_#?D=$tQKef4OTVvU`-^rJ5MC*)yuGS7DeSyGU zeZAFwfmYv>Z4_b_aV8azyxmZR282qlME=UrqtaJhDh0jQlY z-fGR9Ju~Uf9G{l^;PmP3ePA7aCXSSx17*K*qZ-TYJj){<=b{E( z4lA4$!rOFN(3?UUC1-z+U!Dfnw!4bA>gY4N-J#}sKcOTw6=1Snje{O3rXn ziFp()@gx)>XDe3{Jc$c}t}n1Pj(6O^Q@B`{L&+r?Cy0HVe#t7gyrp~cO5OC94)Z8(tC20+%!-&eNfkRs!Z+IH5v zkd9h#6c5OUc$jL?ggbB-?!kBP6Z{Il!(U{O93-!kqvQm6mz*OXkWWaS6v+ztl&ot0 zKfEUk*bBKDF1_$^ng7o}{rCSD_3_NQjRHo2=TLwQ&K2fz z=-Bx}_Htyd?P9%-RTk;j)~G1hsB|1rrQ?WOe;A_eqO0V1DYi9Y50?M_hXC{aSHhd- HrB>iE&98X^ diff --git a/.gitignore b/.gitignore index d2bbec7..9981f52 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ pubspec.lock # If you don't generate documentation locally you can remove this line. doc/api/ -.idea \ No newline at end of file +.idea +.DS_Store \ No newline at end of file From 9d6d6fe5b624e2448740c4ab08e3e1c42a1948d5 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Fri, 18 Nov 2022 11:15:53 +0000 Subject: [PATCH 27/33] Create Table of Contents using https://markdown-all-in-one.github.io/docs/guide/table-of-contents.html#overview --- README.md | 97 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 78 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 82145de..5f0bf60 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,73 @@ ![Learn](https://user-images.githubusercontent.com/17494745/200544789-0b024c77-0d49-4702-8866-b69f61521033.png) -Learn the Flutter basics to get up-and-running **fast** and build **awesome cross-platform applications**! +Learn the **`Flutter`** basics to get up-and-running **fast** +and build **awesome cross-platform applications**! + +- [What? πŸ’‘](#what-) +- [Why? 🀷](#why-) +- [Who? πŸ‘€](#who-) + - [Mac Focussed? 🍏](#mac-focussed-) + - ["_`iOS` users spend more than double on subscriptions compared to `Android` users_"](#ios-users-spend-more-than-double-on-subscriptions-compared-to-android-users) +- [Install ⬇️](#install-️) + - [Mac: Homebrew 🍺](#mac-homebrew-) + - [_Manual_ Install](#manual-install) + - [Installing Flutter SDK](#installing-flutter-sdk) + - [Install XCode](#install-xcode) + - [Install Android Studio](#install-android-studio) + - [Installing Cocoapods](#installing-cocoapods) + - [Adding plugins to Android Studio](#adding-plugins-to-android-studio) + - [Checking everything](#checking-everything) + - [Installing for Windows devices](#installing-for-windows-devices) +- [Core Principles 🐣](#core-principles-) + - [Widgets](#widgets) + - [Stateless widgets](#stateless-widgets) + - [Stateful widgets](#stateful-widgets) + - [Layout](#layout) + - [Assets](#assets) + - [Navigation and routing](#navigation-and-routing) + - [Networking](#networking) + - [Local databases](#local-databases) + - [SQLite](#sqlite) + - [1. Add the dependencies](#1-add-the-dependencies) + - [2. Define a model](#2-define-a-model) + - [3. Open connection to the database](#3-open-connection-to-the-database) + - [4. Creating table](#4-creating-table) + - [5. CRUD operations](#5-crud-operations) + - [ObjectBox](#objectbox) + - [State management](#state-management) + - [Dependency injection](#dependency-injection) +- [Testing πŸ§ͺ](#testing-) + - [Unit testing](#unit-testing) + - [Mock testing](#mock-testing) + - [Integration testing](#integration-testing) +- [App demo πŸ“±](#app-demo-) + - [0. Setting up a new project](#0-setting-up-a-new-project) + - [1. Project structure](#1-project-structure) + - [2. Creating a list of todos](#2-creating-a-list-of-todos) + - [3. Adding interactivity](#3-adding-interactivity) + - [4. Adding navigation](#4-adding-navigation) + - [5. Finishing touches](#5-finishing-touches) + - [6. Testing!](#6-testing) + - [6.1 Unit testing](#61-unit-testing) + - [6.2 Widget testing](#62-widget-testing) + - [6.3 Test coverage](#63-test-coverage) +- [Final remarks πŸ‘‹](#final-remarks-) + + +
+ # What? πŸ’‘ -Flutter is an open-source framework created by Google +**`Flutter`** is an open-source framework created by Google for creating multi-platform, high-performance applications from a single codebase. It makes it easier for you to build user interfaces that works both on web and mobile devices. -`Flutter` uses +**`Flutter`** uses [`Dart`](https://github.com/dwyl/learn-dart), a general-purpose programming language created by **Google**. If you come from an object-oriented programming language @@ -23,7 +78,7 @@ you will feel right at home. # Why? 🀷 -1. Flutter can be used to build cross platform **native** applications +1. **`Flutter`** can be used to build cross platform **native** applications (Android, iOS, Desktop and Web) using the _same_ codebase. This significantly simplifies maintenance costs and dev headache when deploying for either Android or iOS devices. @@ -35,7 +90,7 @@ Being a language optimized for UI and compiling to ARM & x64 machine code for mobile, desktop and backend, it offers amazing performance benchmarks. -3. Development times are significantly faster +3. **Development times** are **_significantly_ faster** than other cross-platform frameworks thanks to stateful hot-reloading and excellent virtual device support. @@ -83,8 +138,8 @@ The _reason_ we use **`Mac`** is simple: it's the _only_ way to ship apps for **`iOS`**. Like it or not, **`iPhone`** now has a -**`50%` Market Share in the US**: -https://www.visualcapitalist.com/iphone-majority-us-smartphones/ +**`50%` Market Share in the US**:
+[visualcapitalist.com/iphone-majority-us-smartphones](https://www.visualcapitalist.com/iphone-majority-us-smartphones/) ![iphone-americas-top-smartphone](https://user-images.githubusercontent.com/194400/202679987-28743fa1-45c7-455b-a8b8-ca2f29567628.jpg) @@ -95,12 +150,12 @@ with and [**`68.89%`**](https://gs.statcounter.com/os-market-share/mobile/norway) respectively. -https://mezha.media/en/2022/10/10/percentage-of-iphone-users-in-different-european-countries/ +[mezha.media/en/2022/10/10/percentage-of-iphone-users-in-different-european-countries](https://mezha.media/en/2022/10/10/percentage-of-iphone-users-in-different-european-countries/) ![europe-iphone-market-share](https://user-images.githubusercontent.com/194400/202684011-b58184e6-3501-42f2-ad63-cca14c8e828f.png) -Worldwide **`iPhone`** a **`~30%` Market Share**: -https://gs.statcounter.com/os-market-share/mobile/worldwide +Worldwide **`iPhone`** has a **`~30%` Market Share**: +[gs.statcounter.com/os-market-share/mobile/worldwide](https://gs.statcounter.com/os-market-share/mobile/worldwide) Mostly because there are _many_ cheap Android devices that have flooded the market. @@ -109,13 +164,16 @@ from an Native Mobile App development perspective is: ### "_`iOS` users spend more than double on subscriptions compared to `Android` users_" -https://www.phonearena.com/news/app-store-users-spend-more-than-double-google-play-users-subscriptions_id138692 +[phonearena.com/news/app-store-users-spend-more-than-double-google-play-users-subscriptions_id138692](https://www.phonearena.com/news/app-store-users-spend-more-than-double-google-play-users-subscriptions_id138692) So ... if you're building a **`SaaS` product**, -you should pour _most_ of your effort into perfecting the UI/UX on **`iPhone`**. +you should focus _most_ of your effort +on perfecting the UI/UX on **`iPhone`**. -This is why we use **`Mac`**computers for our **`Flutter`** dev. -So we can run **`XCode`** and test on **`iOS`** devices +This is why we use **`Mac`**computers +for our **`Flutter`** dev work. +So we can run **`XCode`** +and test on **`iOS`** devices and pay our bills. @@ -124,10 +182,11 @@ and pay our bills. ## Mac: Homebrew 🍺 -The easiest way to install `Flutter` -on a Mac is using [Homebrew](https://brew.sh/). -After installing Homebrew, -you can install `Flutter` +The easiest way to install **`Flutter`** +on a Mac is using **`Homebrew`**: +[brew.sh](https://brew.sh) +After installing `brew`, +you can install **`Flutter`** by running the command: ```sh @@ -147,7 +206,7 @@ You should see something similar to: -## Manual Install +## _Manual_ Install Installing Flutter might seem like a daunting task. But do not worry, we'll help you get your local environment From 452a8446ef51af3c835a72a4b2a03fcc636b6b42 Mon Sep 17 00:00:00 2001 From: LuchoTurtle Date: Fri, 18 Nov 2022 13:52:46 +0000 Subject: [PATCH 28/33] Fixing typos. #65 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5f0bf60..a21d28e 100644 --- a/README.md +++ b/README.md @@ -3116,8 +3116,8 @@ around 20 minutes! Give yourself a pat on the back! :tada: If you wish to learn a bit more, take a look at this repo's `guides` folder to learn about [logging in with Firebase](./guides/login-firebase-tutorial.md) -or [webviews in Flutter](./guides/webview-tutorial.md) +or [webviews in Flutter](./guides/webview-tutorial.md). -If you want to see more fully tested, check these out! +If you want to see more fully tested projects, check these out! - [flutter-todo-list-tutorial](https://github.com/dwyl/flutter-todo-list-tutorial) -- [flutter-counter-example](https://github.com/dwyl/flutter-counter-example) \ No newline at end of file +- [flutter-counter-example](https://github.com/dwyl/flutter-counter-example) From 3dffe614097d5ee50a1426d06489e9bf762f6089 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Fri, 18 Nov 2022 13:57:15 +0000 Subject: [PATCH 29/33] tidy Why section #68 --- README.md | 51 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a21d28e..1aa91ff 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,14 @@ and build **awesome cross-platform applications**! - [Why? 🀷](#why-) - [Who? πŸ‘€](#who-) - [Mac Focussed? 🍏](#mac-focussed-) - - ["_`iOS` users spend more than double on subscriptions compared to `Android` users_"](#ios-users-spend-more-than-double-on-subscriptions-compared-to-android-users) + - ["_`iOS` users `spend` more than `double` on `subscriptions` compared to `Android` users_"](#ios-users-spend-more-than-double-on-subscriptions-compared-to-android-users) - [Install ⬇️](#install-️) - [Mac: Homebrew 🍺](#mac-homebrew-) - [_Manual_ Install](#manual-install) - [Installing Flutter SDK](#installing-flutter-sdk) - - [Install XCode](#install-xcode) + - [Install `XCode`](#install-xcode) - [Install Android Studio](#install-android-studio) - - [Installing Cocoapods](#installing-cocoapods) + - [Installing `Cocoapods`](#installing-cocoapods) - [Adding plugins to Android Studio](#adding-plugins-to-android-studio) - [Checking everything](#checking-everything) - [Installing for Windows devices](#installing-for-windows-devices) @@ -99,7 +99,7 @@ when we open it again we can continue from where we stopped. 4. `Flutter` has a **_complete_ design system** -with a library of Material UI widgets +with a library of **Material UI widgets** included which speeds up the development process. @@ -108,7 +108,7 @@ and is wildly used in production worldwide. ![fast-pace](https://user-images.githubusercontent.com/194400/84572723-e3b04800-ad93-11ea-85e2-19e9693e5a26.png) -`Flutter` has overtaken React Native in Google searches, +`Flutter` overtook React Native 2020 in Google searches, further showcasing the growing trend of `Flutter`: https://trends.google.com/trends/explore?date=today%205-y&q=flutter,react%20native @@ -144,7 +144,10 @@ Like it or not, **`iPhone`** now has a ![iphone-americas-top-smartphone](https://user-images.githubusercontent.com/194400/202679987-28743fa1-45c7-455b-a8b8-ca2f29567628.jpg) In Europe, **`iPhone`** ownership/use correlates strongly to wealth of the nation; -Monaco and Norway the two countries with the highest GDP/Capita top the table +[Monaco](https://en.wikipedia.org/wiki/Monaco#Economy) +and +[Norway](https://en.wikipedia.org/wiki/Norway#Economy) +the two countries with the highest GDP/Capita top the table with [**`69.91%`**](https://www.reddit.com/r/MapPorn/comments/xx4gp6/percentage_of_iphone_users_in_europe/) and @@ -162,20 +165,26 @@ that have flooded the market. But by _far_ the most important fact/stat to pay attention from an Native Mobile App development perspective is: -### "_`iOS` users spend more than double on subscriptions compared to `Android` users_" +### "_`iOS` users `spend` more than `double` on `subscriptions` compared to `Android` users_" [phonearena.com/news/app-store-users-spend-more-than-double-google-play-users-subscriptions_id138692](https://www.phonearena.com/news/app-store-users-spend-more-than-double-google-play-users-subscriptions_id138692) -So ... if you're building a **`SaaS` product**, +So ... if you're building a +[**`SaaS` product**](https://github.com/dwyl/product-roadmap#why-are-we-building-an-app), you should focus _most_ of your effort on perfecting the UI/UX on **`iPhone`**. -This is why we use **`Mac`**computers +This is _why_ we use **`Mac`** computers for our **`Flutter`** dev work. So we can run **`XCode`** and test on **`iOS`** devices and pay our bills. - +We would _much_ rather use +a fully Open Source Hardware/Software platform. +e.g: +[Framework](https://github.com/dwyl/hq/issues/565); +We _love_ their +[Mission](https://frame.work/about) # Install ⬇️ @@ -184,10 +193,10 @@ and pay our bills. The easiest way to install **`Flutter`** on a Mac is using **`Homebrew`**: -[brew.sh](https://brew.sh) -After installing `brew`, +[brew.sh](https://brew.sh)
+After you've installed `brew`, you can install **`Flutter`** -by running the command: +with the command: ```sh brew install --cask flutter @@ -196,8 +205,8 @@ brew install --cask flutter You should see something similar to: ```sh -==> Downloading https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_maco -######################################################################## 100.0% +==> Downloading https://storage.googleapis.com/releases/stable/macos/flutter +#################################################################### 100.0% ==> Installing Cask flutter ==> Linking Binary 'dart' to '/opt/homebrew/bin/dart' ==> Linking Binary 'flutter' to '/opt/homebrew/bin/flutter' @@ -246,10 +255,10 @@ terminal window. It checks it all the necessary tools for development for all devices are correctly installed. Let's do just that. -## Install XCode +## Install `XCode` If you don't already have **`XCode`** installed, -open your **`AppStore`**, search for 'XCode' +open your **`AppStore`**, search for `"XCode"` and press `Install`. It's that easy. image @@ -264,8 +273,8 @@ the installer do its magic. After this, you should be prompted with the followin image -Click on the `More actions` dropdown and click on `SDK Manager`. -You should be prompted with this window. +Click on the `More actions` dropdown and click on `SDK Manager`.
+You should be prompted with this window: Screenshot 2022-11-08 at 11 41 29 @@ -294,7 +303,8 @@ Restart your terminal again and type `flutter doctor --android-licenses`. This will prompt you to accept the Android licenses. Just type `y` as you read through them to accept. -## Installing Cocoapods +## Installing `Cocoapods` + If you run `flutter doctor` again, you should see we are almost done. You might see a text saying `CocoaPods not installed`. Let's fix that. @@ -303,6 +313,7 @@ Install [Homebrew](https://brew.sh/) and run `brew install cocoapods`. And you should be all sorted! ## Adding plugins to Android Studio + If you happen to use Android Studio when developing, adding the Flutter plugin will help you tremendously. Just open Android Studio, click on `Plugins`, From 5f91ad77a582b06bad7f5b24d1e67bd981aaf9f3 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Mon, 21 Nov 2022 12:04:15 +0000 Subject: [PATCH 30/33] Fix typos and formatting #68 --- README.md | 155 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 95 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 1aa91ff..dad42fa 100644 --- a/README.md +++ b/README.md @@ -506,19 +506,25 @@ class MyAppBar extends StatelessWidget { We notice straight away the widget is a subclass of [`StatelessWidget`](https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html). -All widgets have a `Key key` (`super.key`) as optional parameter in their ocnstructor. -It is by the flutter engine at the step of recognizingg which widget -in a list as changed. It's more useful when you have a list of widgets -*of the same type* that can potentially be removed or inserted. +All widgets have a `Key key` (`super.key`) +as an optional parameter in their constructor. +The `key` is used by the **`Flutter` engine** +at the step of recognizing which widget +in a list has changed. +It's more useful when you have a list of widgets +*of the same type* +that can potentially be removed or inserted. This `MyAppBar` widget takes as argument a `title`. This effectively becomes the `field` of the widget, and is used in the `Expanded` children widget. Additionally, since this is a widget (more specifically, a subclass of `Stateless Widget`), we have to -implement the `build()` function. This is what is rendered. +implement the `build()` function. +This is what is rendered. -This widget could be used in a container and be one of its childrens +This widget could be used in a container +and be one of its children like so: ```dart @@ -530,20 +536,25 @@ like so: ``` Simple enough, right? +Here the `MyAppBar` is the parent widget, +`title` is a property +and `Text` is the child widget. ### Stateful widgets + While stateless widgets are static (never change), -**stateful widgets** are dynamic. For example, -they change its appearance or behaviour according -to events triggered by user interaction or when -it receives data. +**stateful widgets** are dynamic. +For example: they change appearance or behavior +according to events triggered by user interaction +or when it receives data. -For example `Checkbox`, `Slider`, `Textfield` are examples -of stateful widgets - subclass of +For example `Checkbox`, `Slider`, `Textfield` +are examples of [`StatefulWidget`](https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html). A widget's state is stored in a `State` object. -Therefore, we *separate* the widget's state from its appearance. -Whenever the state changes, the `State` object calls `setState()`, +Therefore, we _separate_ the widget's state from its appearance. +Whenever the state changes, +the `State` object calls `setState()`, thus rerendering the widget. Let's see some code! @@ -552,9 +563,9 @@ Let's see some code! import 'package:flutter/material.dart'; class Counter extends StatefulWidget { - // This is the state object, different from the appearance. - // It holds the state configuration and - // the values provided by the parent and used by the build method + // Counter is the Stateful Widget, different from the appearance. + // It holds the state configuration and values + // provided by the parent and used by the build method // of the State (no values are provided in this instance) // Fields in a Widget subclass are always marked @@ -615,19 +626,25 @@ void main() { } ``` -Let's unpack the code above. The `StatefulWidget` and `State` are separate objects. -The former (being the first one) declares its state by using the State object. -The State object is declared right after, initializing a `_counter` at 0. +Let's unpack the code above. +The `StatefulWidget` and `State` are separate objects. +The former (being the first one) +declares its' state by using the `State` object. +The `State` object is declared right after, initializing an `int _counter` at `0`. It declares an `_increment()` function that calls `setState()` (indicating the state is going to be changed) and increments the `_counter` variable. -As with any widget, the `build()` method makes use of the `_counter` variable -to display the number of times the button is pressed. Everytime it is pressed, -the `_increment()` function is called, effectively changing the state and incrementing it. +As with any widget, the `build()` method +makes use of the `_counter` variable +to display the number of times the button is pressed. +Everytime it is pressed, +the `_increment()` function is called, +effectively changing the state and incrementing it. ## Layout -As we've already stated, the core of Flutter are widgets. +As we've already stated, +the core of `Flutter` are widgets. In fact, almost everything is a widget - even layout models. The things you see are widgets. @@ -637,30 +654,35 @@ But things that you *don't see* are also widgets. We mentioned this before but we'll understand it better now. For any web or mobile app development, we need to create layouts to organize our components in and -make it look *shiny* :shiny: and *good-looking* :art:. +make it look _shiny_ ✨ and _good-looking_ 🎨. -This example is taken from the official docs --> https://docs.flutter.dev/development/ui/layout#lay-out-a-widget +This example is taken from the official docs: +https://docs.flutter.dev/development/ui/layout#lay-out-a-widget Layout | Layout with padding and delimited borders :-------------------------:|:-------------------------: ![](https://docs.flutter.dev/assets/images/docs/ui/layout/lakes-icons.png) | ![](https://docs.flutter.dev/assets/images/docs/ui/layout/lakes-icons-visual.png) -So, you may ask, **how many widgets are there in this menu**? -Great question! There are visible widgets but also widgets that +So, you may ask, +**how many widgets are there in this menu**? +Great question!
+There are visible widgets but also widgets that *help us* lay out the items correctly, center them and space them evenly to make it look good. -Here's how the widget tree looks like for this menu. + +Here's how the widget tree looks like for this menu: ![widget_tree](https://docs.flutter.dev/assets/images/docs/ui/layout/sample-flutter-layout.png) -The nodes in *pink* are containters. They are *not visible* -but help us customize its child widget by adding +The **pink nodes** are **containers**. +They are **_not_ visible** +but help us **customize** its **child widget** +by adding `padding`, `margin`, `border`, `background color`, etc... Let's see a code example of an invisible widget that will -center a text in the middle of the screen. +center a block of `text` in the middle of the screen: ```dart class MyApp extends StatelessWidget { @@ -687,23 +709,26 @@ class MyApp extends StatelessWidget { The `Center` widget centers all its children inside of it. `Center` is *invisible* but is a widget nonetheless. -This yields the following result. +This yields the following result: -![invisible_result](https://docs.flutter.dev/assets/images/docs/ui/layout/hello-world.png) +![centered-text](https://user-images.githubusercontent.com/194400/203043079-7c9be65a-0b2b-4580-9dac-15f42ef3fb25.png) -See? Isn't it so simple? :tada: +See? Isn't it so simple? πŸŽ‰ -As you would do in React, you can whatever Layout you wish just -by encapsulating widgets (akin to components) and ordering -them accordingly. +You can create _any_ Layout you wish just +by encapsulating widgets +and ordering them accordingly. ## Assets -For any application, sometimes we need images and assets to display to the user. -Common resources are image files, static data (`JSON` files), -videos, icons... -In Flutter, we use the `pubspec.yaml` file +Sometimes we need images and assets to be displayed in our App. +Common resources are: +image files, +static data (`JSON` files), +videos, buttons and icons. + +In Flutter, we use a `pubspec.yaml` file (often located at the root of the project) to require assets in the app. @@ -728,20 +753,22 @@ flutter: > Both are imported and included in the asset bundle. > One is considered the **main asset** and the other > a **variant**. -> This behaviour is useful for images on different resolutions. +> This behavior is useful for images of different resolutions. There are two ways of accessing the loaded access. -Each Flutter app has a `RootBundle` for easy access -to the main asset bundle. You can import directly +Each `Flutter` app has a `RootBundle` +for easy access to the main asset bundle. +You can import directly using the `rootBundle` global static. -However, inside a widget context, it's recommended to obtain the -asset bundle for the widget `BuildContext` using the +However, inside a widget context, +it's recommended to obtain the asset bundle +for the widget `BuildContext` using the [`DefaultAssetBundle`](https://api.flutter.dev/flutter/widgets/DefaultAssetBundle-class.html). This approach allows the parent widget to substitute a different asset bundle at runtime, which is useful for localization or testing purposes. -Here's a code example for the `rootBundle` approach. +Here's a code example for the `rootBundle` approach: ```dart import 'package:flutter/services.dart' show rootBundle; @@ -751,8 +778,9 @@ Future loadAsset() async { } ``` -Here's a code example for the recommended approach inside -a widget. +Here's a code example +for the recommended approach +inside a widget: ```dart String data = await DefaultAssetBundle.of(context).loadString("assets/data.json"); @@ -760,18 +788,25 @@ final jsonResult = jsonDecode(data); //latest Dart ``` ## Navigation and routing -Most web and mobile apps aren't just a single page. -The user needs to navigate between screens to do whatever -action needs to be done, be it checking the details of a -product or just wanting to see the shopping cart. -Flutter provides a `Navigator` widget to display screns as a stack, +Most web and mobile apps aren't just a single page. +The person using the app +needs to navigate between screens to do +whatever action needs to be done, +be it checking the details of a product +or just wanting to see the shopping cart. + +`Flutter` provides a `Navigator` widget +to display screens as a stack, using the native transition animations of the target device. Navigating between screens necessitates the route's -`BuildContext` (which can be accessed through the widget) and -is made by calling methods like `push()` and `pop()`. +`BuildContext` (which can be accessed through the widget) +and is made by calling methods like +`push()` +and +`pop()`. -Here's a code showcasing navigating between two routes. +Here's code showcasing navigating between two routes: ```dart import 'package:flutter/material.dart'; @@ -829,7 +864,7 @@ class SecondRoute extends StatelessWidget { } ``` -This basic code example shocases two routes, +This basic code example showcases two routes, each one containing only a single button. Tapping the one on the first route will navigate to the second route. From 7e1dd0168c2fb7d72577d401e4dfea747feabf65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 13 Dec 2022 12:06:54 +0000 Subject: [PATCH 31/33] fix: Formatting and fixing typos. #68 --- README.md | 1185 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 709 insertions(+), 476 deletions(-) diff --git a/README.md b/README.md index dad42fa..6478a33 100644 --- a/README.md +++ b/README.md @@ -458,6 +458,7 @@ There are several other widgets that have a similar behaviour, such as padding, alignment, row, columns, and grids. ### Stateless widgets + Widgets are not all stateless. Stateless widgets never change. They receive arguments from their parent, store them in `final` member variables (`final` is analogous to a `const`ant variable). When a widget is asked @@ -548,11 +549,14 @@ For example: they change appearance or behavior according to events triggered by user interaction or when it receives data. -For example `Checkbox`, `Slider`, `Textfield` +For example: +`Checkbox`, `Slider`, `Textfield` are examples of [`StatefulWidget`](https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html). A widget's state is stored in a `State` object. -Therefore, we _separate_ the widget's state from its appearance. +Therefore, +we _separate_ the widget's state +from its appearance. Whenever the state changes, the `State` object calls `setState()`, thus rerendering the widget. @@ -626,16 +630,20 @@ void main() { } ``` -Let's unpack the code above. +Let's unpack this. The `StatefulWidget` and `State` are separate objects. The former (being the first one) -declares its' state by using the `State` object. -The `State` object is declared right after, initializing an `int _counter` at `0`. -It declares an `_increment()` function that calls `setState()` -(indicating the state is going to be changed) and increments the `_counter` variable. - -As with any widget, the `build()` method -makes use of the `_counter` variable +declares its state +by using the `State` object. +The `State` object is declared right after, +initializing an `int _counter` at `0`. +It declares an `_increment()` function +that calls `setState()` +(indicating the state is going to be changed) +and increments the `_counter` variable. + +As with any widget, +the `build()` method makes use of the `_counter` variable to display the number of times the button is pressed. Everytime it is pressed, the `_increment()` function is called, @@ -645,16 +653,19 @@ effectively changing the state and incrementing it. As we've already stated, the core of `Flutter` are widgets. -In fact, almost everything is a widget - even layout models. +In fact, almost everything is a widget - +even layout models. The things you see are widgets. ![image](https://user-images.githubusercontent.com/17494745/200579851-de25d19d-5c80-4033-8491-c2ff452f7137.png) But things that you *don't see* are also widgets. -We mentioned this before but we'll understand it better now. +We mentioned this before +but we'll understand it better now. For any web or mobile app development, -we need to create layouts to organize our components in and -make it look _shiny_ ✨ and _good-looking_ 🎨. +we need to create layouts to organize our components +and make them look _shiny_ ✨ +and _good-looking_ 🎨. This example is taken from the official docs: https://docs.flutter.dev/development/ui/layout#lay-out-a-widget @@ -666,10 +677,15 @@ Layout | Layout with padding and delimited borders So, you may ask, **how many widgets are there in this menu**? -Great question!
-There are visible widgets but also widgets that -*help us* lay out the items correctly, center them and space -them evenly to make it look good. +Great question! + +In the pictures above, +there are *visible* widgets +but also widgets that *help us* lay out the items correctly, +center them +and space them evenly to make +everything look good. +These widgets **are not visible**. Here's how the widget tree looks like for this menu: @@ -677,12 +693,14 @@ Here's how the widget tree looks like for this menu: The **pink nodes** are **containers**. They are **_not_ visible** -but help us **customize** its **child widget** +but help us **customize** +their **child widget** by adding `padding`, `margin`, `border`, `background color`, etc... -Let's see a code example of an invisible widget that will -center a block of `text` in the middle of the screen: +Let's see a code example of an invisible widget +that will center a block of `text` +in the middle of the screen: ```dart class MyApp extends StatelessWidget { @@ -708,7 +726,8 @@ class MyApp extends StatelessWidget { ``` The `Center` widget centers all its children inside of it. -`Center` is *invisible* but is a widget nonetheless. +`Center` is *invisible* +but is a widget nonetheless. This yields the following result: ![centered-text](https://user-images.githubusercontent.com/194400/203043079-7c9be65a-0b2b-4580-9dac-15f42ef3fb25.png) @@ -722,15 +741,17 @@ and ordering them accordingly. ## Assets -Sometimes we need images and assets to be displayed in our App. +Sometimes, +we need images and assets to be displayed in our App. Common resources are: image files, static data (`JSON` files), videos, buttons and icons. -In Flutter, we use a `pubspec.yaml` file -(often located at the root of the project) to -require assets in the app. +In Flutter, +we use a `pubspec.yaml` file +(often located at the root of the project) +to require assets into our app. ```yaml flutter: @@ -740,8 +761,11 @@ flutter: ``` > There's a nuanced behavior when loading assets. -> If you have two files ` .../graphics/background.png` and -> `.../graphics/dark/background.png` and the `pubspec.yaml` file +> +> If you have two files +> ` .../graphics/background.png` +> and `.../graphics/dark/background.png` +> and the `pubspec.yaml` file > contains the following: > ```yaml @@ -751,21 +775,23 @@ flutter: > ``` > Both are imported and included in the asset bundle. -> One is considered the **main asset** and the other -> a **variant**. +> One is considered the **main asset** +> and the other a **variant**. > This behavior is useful for images of different resolutions. -There are two ways of accessing the loaded access. +There are two ways of accessing the loaded assets. Each `Flutter` app has a `RootBundle` for easy access to the main asset bundle. You can import directly using the `rootBundle` global static. + However, inside a widget context, it's recommended to obtain the asset bundle for the widget `BuildContext` using the [`DefaultAssetBundle`](https://api.flutter.dev/flutter/widgets/DefaultAssetBundle-class.html). -This approach allows the parent widget to substitute a different -asset bundle at runtime, which is useful for localization +This approach allows the parent widget +to substitute a different asset bundle at runtime, +which is useful for localization or testing purposes. Here's a code example for the `rootBundle` approach: @@ -784,29 +810,29 @@ inside a widget: ```dart String data = await DefaultAssetBundle.of(context).loadString("assets/data.json"); -final jsonResult = jsonDecode(data); //latest Dart +final jsonResult = jsonDecode(data); ``` ## Navigation and routing Most web and mobile apps aren't just a single page. The person using the app -needs to navigate between screens to do -whatever action needs to be done, +needs to navigate between screens +to do whatever action needs to be done, be it checking the details of a product or just wanting to see the shopping cart. `Flutter` provides a `Navigator` widget -to display screens as a stack, +to display screens as a **stack** using the native transition animations of the target device. -Navigating between screens necessitates the route's -`BuildContext` (which can be accessed through the widget) +Navigating between screens +necessitates the route's `BuildContext` +(which can be accessed through the widget) and is made by calling methods like -`push()` -and -`pop()`. +`push()` and `pop()`. -Here's code showcasing navigating between two routes: +Here's code showcasing +the navigation between two routes: ```dart import 'package:flutter/material.dart'; @@ -870,11 +896,14 @@ Tapping the one on the first route will navigate to the second route. Clicking on the button of the second route will return the user to the first route. -We are using the `Navigator.push()` and `Navigator.pop()` -functions to achieve this, by passing the context of -the widget. -Additionally, we are leveraging `MaterialPageRoute` to -transition between routes using a platform-specific animation +We are using +the `Navigator.push()` +and `Navigator.pop()` functions to achieve this, +by passing the context of the widget. +Additionally, +we are leveraging `MaterialPageRoute` +to transition between routes +using a platform-specific animation according to the [Material Design guidelines](https://m3.material.io/). Here's how it should look! @@ -882,23 +911,34 @@ Here's how it should look! ![navigating_gif](https://user-images.githubusercontent.com/17494745/200613079-f65baeee-a822-4a58-b075-ce169d751325.gif) -If your application necessitates advanced navigation and routing requirements +If your application needs advanced navigation +and routing requirements (which is often the case with web apps that use direct links to each screen, -or an app with multiple `Navigator` widgets), you should consider using a +or an app with multiple `Navigator` widgets), +you should consider using a routing package like [`go_router`](https://pub.dev/packages/go_router). -This package allows one to parse the route path and configure the `Navigator` -whenever an app receives, for example, a deep link. +This package allows one to parse the route path +and configure the `Navigator` whenever an app receives, +for example, a deep link. ## Networking -For most apps, fetching data from the internet is a must. -Luckily, fetching data from the internet is a breeze. Let's do it! -Firstly, we need to add the [`http`](https://pub.dev/packages/http) -package to the dependencies section in the `pubspec.yaml` file. -This file can be found at the route of your project. +For most apps, +fetching data from the internet is a must. +Luckily, +fetching data from the internet is a breeze. +Let's do it! + +Firstly, +we need to add the [`http`](https://pub.dev/packages/http) package +to the dependencies section +in the `pubspec.yaml` file. +This file can be found +at the route of your project. -Let's add the package to the dependency list and import it. +Let's add the package to the dependency list +and import it. ```yaml dependencies: @@ -909,9 +949,11 @@ dependencies: import 'package:http/http.dart' as http; ``` -We also need to change the `AndroidManifest.xml` file to -add Internet permission on Android devices. This file can be found in the -`android/app/src/main` on newly created projects. Add the following line. +We also need to change the `AndroidManifest.xml` file +to add Internet permission on Android devices. +This file can be found in the `android/app/src/main` +on newly created projects. +Add the following line. ```xml @@ -919,8 +961,10 @@ add Internet permission on Android devices. This file can be found in the ``` -Now, to make a network request is as easy -as apple pie. Check the following code. +Now, +to make a network request +is as easy as apple pie. +Check the following code. ```dart Future fetchAlbum() { @@ -928,13 +972,17 @@ Future fetchAlbum() { } ``` -By calling `http.get()`, it returns a [`Future`](https://github.com/dwyl/learn-dart#asynchronous-events) -that contains a `Response`. `Future` is a class to work with async operations. +By calling `http.get()`, +it returns a [`Future`](https://github.com/dwyl/learn-dart#asynchronous-events) +that contains a `Response`. +`Future` is a class to work with async operations. It represents a potential value that will occur in the future. -While `http.Response` has our data, it's much more useful to translate it -to a logical class. We can convert `http.Response` to a `Todo` class, -representing a "todo item". Let's create that class! +While `http.Response` has our data, +it's much more useful to translate it to a logical class. +We can convert `http.Response` +to a `Todo` class, representing a "todo item". +Let's create that class! ```dart class Todo { @@ -958,10 +1006,11 @@ class Todo { } ``` -We can create a function that makes the http request and, -if it is successful, tries to parse the data and create a -`Todo` object or raise an an error if the http request is -unsuccessful. +We can create a function that makes the HTTP request and, +if it is successful, +tries to parse the data and create a `Todo` object +or raise an an error +if the HTTP request is unsuccessful. ```dart Future fetchTodos() async { @@ -1001,9 +1050,10 @@ class _MyAppState extends State { } ``` -Finally, to display the data, we would want to use the -`FutureBuilder` widget. As the name implies, it's a -widget made to handle async data operations. +Finally, to display the data, +we would want to use the `FutureBuilder` widget. +As the name implies, +it's a widget made to handle async data operations. ```dart FutureBuilder( @@ -1022,25 +1072,31 @@ FutureBuilder( ``` The `future` paramter relates to object we want to work with. -In this case, it is a parsed `Todo` object. +In this case, +it is a parsed `Todo` object. The `builder` function tells Flutter what needs to be rendered, -depending on the current state of `Future`, which can -be *loading*, *success* or *error*. -Depending on the result of the operation, we -either show the error, the data or a loading animation +depending on the current state of `Future`, +which can be *loading*, *success* or *error*. +Depending on the result of the operation, +we either show the error, +the data +or a loading animation while we wait for the http request to fulfill. -Isn't it easy? =) +Isn't it easy? πŸ˜ƒ ## Local databases -Sometimes, when writing an app, we need to persist -and query large amounts of data on the local device. -In these cases, it is beneficial considering -using a database instead of a local file or a key-value store. -In this walkthrough, we are going to present -two alternatives: SQLite and ObjectBox. +Sometimes, when writing an app, +we need to persist and query large amounts of data on the local device. +In these cases, +it is beneficial considering using a database +instead of a local file or a key-value store. + +In this walkthrough, +we are going to present two alternatives: +SQLite and ObjectBox. ### SQLite @@ -1052,11 +1108,11 @@ Sqflite is one of the most used and updated packages to connect to SQLite databases in Flutter. #### 1. Add the dependencies -To work with SQLite databases, we need -to import two dependencies. + +To work with SQLite databases, +we need to import two dependencies. We'll use `sqflite` to interact with the SQLite database, -and `path` to define the location for storing the database -on disk. +and `path` to define the location for storing the database on disk. ```dart @@ -1078,9 +1134,10 @@ import 'package:sqflite/sqflite.dart'; ``` #### 2. Define a model + Let's take a look at the data we are going to store. -Let's define a class for the table we are going to create -in SQLite. +Let's define a class for the table +we are going to create in SQLite. ```dart class Item { @@ -1114,9 +1171,10 @@ class Item { ``` #### 3. Open connection to the database + To open a connection to the SQLite database, -we are going to define the path to the database file -using `path` +we are going to define the path +to the database file using `path` **and** open the database with `sqflite`. @@ -1134,10 +1192,13 @@ final database = openDatabase( ``` #### 4. Creating table -To create the table to store our items, we must first -verify the number of columns and type refer -exactly to the ones we defined in the class. -After this, it's just a matter of running the appropriate + +To create the table to store our items, +we must first verify +the number of columns and type refer exactly +to the ones we defined in the class. +After this, +it's just a matter of running the appropriate `SQL` expression to create the table. ```dart @@ -1160,9 +1221,10 @@ final database = openDatabase( #### 5. CRUD operations -Now that we have a database created, alongside the -table, to create, update, list and insert Items is -quite easy! Check the following piece of code. +Now that we have a database created, +alongside the table, +to create, update, list and insert Items is quite easy! +Check the following piece of code. ```dart Future crudOperations(Item item) async { @@ -1204,29 +1266,34 @@ Future crudOperations(Item item) async { } ``` -And there you have it! Here is a quick rundown of the -process of creating a database, a table and -applying CRUD operations on it. You can leverage -this database to hold large amounts of data locally -(up to a limit, of course) instead of relying -on common files. +And there you have it! +Here is a quick rundown of the process of creating a database, +a table +and applying CRUD operations on it. +You can leverage this database to hold large amounts of data locally +(up to a limit, of course) +instead of relying on common files. ### ObjectBox -There are alternatives to SQLite, such as Hive and `ObjectBox`. -In this section, we are going to just reference -`ObjectBox` so the user knows there isn't one single + +There are alternatives to SQLite, +such as Hive and `ObjectBox`. +In this section, +we are going to just reference `ObjectBox` +so the you know there isn't one single database option. `ObjectBox` provides a NoSQL database that uses a -pure Dart API, so there is no need to learn -and write SQL expressions. There are performance -advantages to using this library. Make sure -to read the [package docs](https://github.com/objectbox/objectbox-dart#flutter-database-for-fast-dart-object-persistence-) +pure Dart API, +so there is no need to learn and write SQL expressions. +There are performance advantages to using this library. +Make sure to read the +[package docs](https://github.com/objectbox/objectbox-dart#flutter-database-for-fast-dart-object-persistence-) to find out if this option is best for you. -Here is how basic setup and CRUD -operations would work using `ObjectBox`. +Here is how basic setup +and CRUD operations would work using `ObjectBox`. ```dart // Annotate a Dart class to create a box @@ -1261,65 +1328,74 @@ box.remove(person.id); ``` ## State management + We have previously mentioned state within a widget. -In stateful widgets, the state and how/when it changes +In stateful widgets, +the state and how/when it changes determines how many times the widget is rendered. State that can be neatly contained in a single widget is referred as "local state" or **ephemeral state**. -Other parts of the widget tree seldom need to access this kind of state. +Other parts of the widget tree +seldom need to access this kind of state. -However, there is state that is *not ephemeral* +However, +there is state that is *not ephemeral* and usually is needed across many widgets of the app. This shared state is usually called **application state**. -Examples of these are user preferences or a shopping cart -in an e-commerce app. +Examples of these +are user preferences or a shopping cart in an e-commerce app. -Consider the following gif, taken directly -from the `Flutter` docs +Consider the following gif, +taken directly from the `Flutter` docs -> https://docs.flutter.dev/development/data-and-backend/state-mgmt/intro ![cart](https://docs.flutter.dev/assets/images/docs/development/data-and-backend/state-mgmt/state-management-explainer.gif) -Each widget in the widget tree might have its own -local state but there's a piece of *application state* +Each widget in the widget tree might have its own local state, +but there's a piece of *application state* (i.e. shared state) in the form of a cart. This cart is accessible from any widget of the app - -in this case, the `MyCart` widget uses it to list what -item was added to it. +in this case, the `MyCart` widget uses it +to list what item was added to it. There are [many approaches to state management](https://docs.flutter.dev/development/data-and-backend/state-mgmt/options), -so it's up to you to decide which options are best -suited for your use case. Many people recommend +so it's up to you to decide +which options are best suited for your use case. +Many people recommend [`Provider`](https://pub.dev/packages/provider) or [`Riverpod`](https://riverpod.dev/). -[Bloc](https://bloclibrary.dev/#/) is also an increasingly -popular alternative which forces the logic and the UI +[Bloc](https://bloclibrary.dev/#/) +is also an increasingly popular alternative +which forces the logic and the UI to be implemented separately. State management and which alternative is best -is a [big point of contention](https://www.reddit.com/r/FlutterDev/comments/w4osgi/for_you_what_is_the_best_state_management_with/) -between developers. There is no bad option, just choose whichever -you think it's best. +is a [big point of contention](https://www.reddit.com/r/FlutterDev/comments/w4osgi/for_you_what_is_the_best_state_management_with/) between developers. +There is no bad option, +just choose whichever you think it's best. -We shall not delve too much into state management as -shared app state is not a beginner-friendly topic -to learn and is often very opinionated. As long -as you understood *what it is*, it's awesome! :tada: +We shall not delve too much into state management +as shared app state +is not a beginner-friendly topic to learn +and is often very opinionated. +As long as you understood *what it is*, +that's okay. πŸ˜„ ### Dependency injection + You might be wondering what dependency injection has to do with the aforementioned state management libraries. -You'll see why this effects how the code is structure and -how it effects testing. +You'll see why this effects *how* the code is structured +and how it effects testing. > "[Dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) > is a design pattern > in which an object or function > receives other objects or functions that it depends on." -Let's write an example of dependency injection in Flutter +Let's write an example of dependency injection in its simplest form. ```dart @@ -1333,44 +1409,56 @@ class LoginService { class Api {} ``` -Here, the `LoginService` receives the `Api` object -in the constructor, something it depends on. -This is no problem if the `LoginService` is one or -two levels deep from a widget it uses it. -However, it does become a problem when it's -ten levels deep. +Here, +the `LoginService` +receives the `Api` object in the constructor, +something it depends on. +This is no problem +if the `LoginService` is one or two levels deep from a widget it uses it. +However, +it does become a problem when it's ten levels deep. ```sh Widget 1 -> Widget 2 -> Widget 3 -> Widget 4 ``` -Let's consider we have a `Widget X`, that returns a list of albums. -If `Widget 4` needed these list of albums, it would need `Widget X`. -To do this, `Widget X` would need to be passed on -from `Widget 1` all the way to `Widget 4` so `Widget 4` could -use it. This is not sustainable and it can become nightmarish. +Let's consider we have a `Widget X` that returns a list of albums. +If `Widget 4` needed these list of albums, +it would need `Widget X`. +To do this, +`Widget X` would need to be passed on +from `Widget 1` all the way to `Widget 4` +so `Widget 4` could use it. +This is not sustainable and it can become nightmarish. Instead of using a [singleton](https://en.wikipedia.org/wiki/Singleton_pattern) -which can often lead to unexpected behaviour and -harder to test codebase, we need to use *dependency injection*. -But in cases of deeply nested widgets, using packages like +which can often lead to unexpected behaviour +and harder to test codebases, +we need to use *dependency injection*. +But in cases of deeply nested widgets, +using packages like [`get_it`](https://pub.dev/packages/get_it) or [`Riverpod`](https://riverpod.dev/) or [`Provider`](https://pub.dev/packages/provider) -are the way to go, as they give us much better -control over our dependencies without any of the -drawbacks of creating our own singletons with `Singleton.instance`, -allowing us to inject dependencies and accessin values -in deeply nested widgets without chaining dependencies -along the widget tree. +are the way to go, +as they give us much better +control over our dependencies +*without* any of the drawbacks of creating +our own singletons with `Singleton.instance`, +allowing us to inject dependencies +and accessing values in deeply nested widgets +without chaining dependencies along the widget tree. This is also useful for mocking objects in testing. -If you are interested in how you would -implement these, we highly recommend taking a look +If you are interested +in how you would implement these, +we highly recommend taking a look at this video -> https://www.youtube.com/watch?v=vBT-FhgMaWM&ab_channel=FilledStacks . -It's a 10 minute video that explains this topic in -simple terms and shows implementation examples using -`get_it` and `Provider`. Great stuff! +It's a 10 minute video that explains this topic +in simple terms +and shows implementation examples +using `get_it` and `Provider`. +Great stuff! @@ -1379,7 +1467,8 @@ simple terms and shows implementation examples using As in all programming languages, frameworks or platforms, the secret to a successful application is to test it _extensively_. Implementing tests is not only advantageous to catch bugs -but also avoid regression when implementing new features. +but also to avoid regression +when implementing new features. > To learn more about an example of using TDD: > https://github.com/dwyl/flutter-counter-example @@ -1389,11 +1478,13 @@ but also avoid regression when implementing new features. > take a look at the [official docs](https://docs.flutter.dev/testing/debugging) ## Unit testing -Unit testing are handy to verify the behaviour -of a single function/method/class. + +Unit testing are handy +to verify the behaviour of a single function/method/class. Let's add some unit tests in Flutter, shall we? -Firstly, we ought to import the [`test`](https://pub.dev/packages/test) +Firstly, +we ought to import the [`test`](https://pub.dev/packages/test) which offers the core functionality for writing tests in Dart. ```dart @@ -1401,9 +1492,10 @@ dev_dependencies: test: 1.22.0 ``` -And now, let's create a simple class and a referring test -file to test it. Create two files so you have the following -folder structure. +And now, +let's create a simple class +and a referring test file to test it. +Create two files so you have the following folder structure. ``` counter_app/ @@ -1413,7 +1505,8 @@ counter_app/ counter_test.dart ``` -In `counter.dart`, add the following piece of code. +In `counter.dart`, +add the following piece of code. ```dart class Counter { @@ -1425,7 +1518,8 @@ class Counter { } ``` -In `counter_test.dart`, add the following: +In `counter_test.dart`, +add the following: ```dart // Import the test package and Counter class @@ -1457,40 +1551,49 @@ void main() { } ``` -We can group tests using the `group()` function. In each -`test()` we use the `expect()` function to compare -expected assertions. +We can group tests using the `group()` function. +In each `test()`, +we use the `expect()` function +to compare expected assertions. -You can type the following command to run the tests -we just created: +You can type the following command +to run the tests we just created: ```sh flutter test test/counter_test.dart ``` ### Mock testing + Sometimes functions fetch data from web services or databases. -When we are unit testing these, it is inconvenient to do so -because calling external dependencies may slow down -the execution time. Needless to say, this external dependency -may sometimes be down, amongst other scenarios. - -In these situations, it is useful to **mock** -these dependencies. In Flutter, the *de facto* way of -mocking classes and objects is using the -[`mockito`](https://pub.dev/packages/mockito) -package. - -In this small section, we are going to add -this dependency, create a function to test +When we are unit testing these, +it is inconvenient to do so +because calling external dependencies may slow down the execution time. +Needless to say, +this external dependency +may sometimes be down, +amongst other scenarios. +It's generally a **bad practice**. + +In these situations, +it is useful to **mock** +these dependencies. +In Flutter, the *de facto* way +of mocking classes and objects +is using the [`mockito`](https://pub.dev/packages/mockito) package. + +In this small section, +we are going to add this dependency, +create a function to test and mock a test file with a mock `http.Client`. -Firstly, add the `mockito` package to the `pubspec.yaml` -file, along with the `flutter_test` dependency -(will provide core testing functionalities) and the -`http` package for HTTP requests. -Do take note that each test dependency will be -added to the `dev_dependencies` section of the file. +Firstly, +add the `mockito` package to the `pubspec.yaml` file, +along with the `flutter_test` dependency +(will provide core testing functionalities) +and the `http` package for HTTP requests. +Do take note that each test dependency +will be added to the `dev_dependencies` section of the file. ```dart dependencies: @@ -1522,18 +1625,21 @@ Future fetchAlbum(http.Client client) async { } ``` -You might have noticed the `http.Client` is provided -to the argument. This makes it so that the client +You might have noticed +the `http.Client` is provided to the argument. +This makes it so that the client that fetches data changes according to any situation. In Flutter, we can provide an `http.IOClient`. For testing, we can pass a mock `http.Client`. -In a test file, we will add an an annotation to the main function -to generate a `MockClient` class with `mockito`. +In a test file, +we will add an an annotation +to the main function to generate a `MockClient` class with `mockito`. According to the argument passed to the annotation, the generated `MockClient` class will implement it. -When generating, the mocks will be located in a file -named `XX_test.mocks.dart`. We will import this file to use them. +When generating, +the mocks will be located in a file named `XX_test.mocks.dart`. +We will import this file to use them. For now, create a test file where we will add tests. ```dart @@ -1551,8 +1657,10 @@ void main() { Now run `flutter pub run build_runner build`. This command will generate the mocks in `XX_test.mocks.dart`. Now we can use these mocks in our tests! -Let's add two: one for a successful request and -another for a failing one, and catch the raised exception. +Let's add two: +one for a successful request +and another for a failing one, +and catch the raised exception. ```dart import 'package:flutter_test/flutter_test.dart'; @@ -1596,53 +1704,63 @@ void main() { } ``` -In these tests, we are **importing** the generated mocks +In these tests, +we are **importing** the generated mocks (`fetch_album_test.mocks.dart`). -Plus, we create the `MockClient()`, define the behaviour -we expect the mock to do, and then pass it to the function, +Plus, we create the `MockClient()`, +define the behaviour we expect the mock to do, +and then pass it to the function, effectively asserting its output. -We can run the tests and see if they fail or not -by running +We can run the tests +and see if they fail or not by running ```sh flutter test test/fetch_album_test.dart ``` -Congratulations! You just mocked a `http.Client` object -and properly tested a function that used an external dependency. +Congratulations! +You just mocked a `http.Client` object +and properly tested a function +that used an external dependency. `mockito` has many other features. You can read about them [in their documentation](https://pub.dev/packages/mockito). ## Integration testing -While unit testing is useful for testing individual -classes, functions or widgets, they don't -test how all of these *work together*, as a whole. -These tasks are captured and tested -with **integration tests**. + +While unit testing is useful +for testing individual classes, functions or widgets, +they don't test how all of these *work together*, as a whole. +These tasks are captured +and tested with **integration tests**. > There is a concept in Flutter that is **widget testing**. -> Widget testing tests a single widget while -> integration testing can test a complete app or large parts of it. +> Widget testing tests a single widget +> while integration testing can test a complete app or large parts of it. > Integration testing will require *a device* or *emulator*. -> So it should be used sparingly and to capture behaviours +> So, it should be used sparingly +> and to capture behaviours > that were missed by unit testing and widget testing. > -> Implementation-wise, widget testing uses the -> `testWidget` function, much like integration tests. So they can be similar. +> Implementation-wise, +> widget testing uses the `testWidget` function, +> much like integration tests. +> So they *can be similar*. We can luckily leverage the SDK's [`integration_test`](https://github.com/flutter/flutter/tree/main/packages/integration_test) package to do this. Let's start by creating a super simple app. -This app will just have a button and a counter -displaying the number of time the button was clicked. +This app will just have a button +and a counter displaying the number of time the button was clicked. -But, before that, let's add the needed dependencies. -We'll be adding the `integration_test` and `flutter_test` -packages to the `dev_dependencies` section of `pubspec.yaml`. +But, before that, +let's add the needed dependencies. +We'll be adding the `integration_test` +and `flutter_test` packages +to the `dev_dependencies` section of `pubspec.yaml`. ```yaml dev_dependencies: @@ -1652,7 +1770,8 @@ dev_dependencies: sdk: flutter ``` -Now, let's create our app. In `lib/app.dart`, +Now, let's create our app. +In `lib/app.dart`, let's use the following piece of code. ```dart @@ -1727,13 +1846,13 @@ class _MyHomePageState extends State { } ``` -Now, inside `integration_test/app_test.dart`, we are -going to test the action of clicking and checking -if the counter is incremented. For this, we will -initialize a singleton service `IntegrationTestWidgetsFlutterBinding`, -which executes the tests on a physical device and -leverage the `WidgetTester` class to interact -with the widgets. +Now, inside `integration_test/app_test.dart`, +we are going to test the action of clicking +and checking if the counter is incremented. +For this, we will initialize a singleton service `IntegrationTestWidgetsFlutterBinding`, +which executes the tests on a physical device, +and leverage the `WidgetTester` class +to interact with the widgets. ```dart import 'package:flutter_test/flutter_test.dart'; @@ -1770,8 +1889,10 @@ void main() { ``` Running these on mobile devices is the same -process as before - just run `flutter test integration_test`. -However, if you were to run these on a web browser, +process as before - +just run `flutter test integration_test`. +However, +if you were to run these on a web browser, you'd need to download [`ChromeDriver`](https://chromedriver.chromium.org/downloads), create a file in `test_driver/integration_test.dart` with @@ -1788,9 +1909,12 @@ In the first one, we launch `chromedriver` with chromedriver --port=4444 ``` -and in the other, from the root of the project, run flutter -with the drive file path we just created and -targetting the test file we want to test, like so: +and in the other, +from the root of the project, +run `flutter` +with the drive file path we just created +and targeting the test file we want to test, +like so: ```dart flutter drive \ @@ -1799,55 +1923,75 @@ flutter drive \ -d chrome ``` -And you're done! Congratulations, you just -unit *and* integration tested your application. -Awesome work! :tada: +And you're done! +Congratulations, you just unit +*and* integration tested your application. +Awesome work! πŸŽ‰ # App demo πŸ“± -We've learnt a lot about basic Flutter principles. There is no -better way of learning them by creating an app and applying them! -In this section, we'll walk you through to creating an -application that fetches information from a rest API, -lists them and allows the user to choose his favourites. +We've learnt a lot about basic Flutter principles. +There is no better way of learning them +by creating an app and applying them! +In this section, +we'll walk you through +to creating an application that fetches information from a rest API, +lists them +and allows the user to choose his favourites. Let's get cracking! ## 0. Setting up a new project -In this walkthrough we are going to use Visual Studio Code. -We will assume you have this IDE installed, as well as the -`Flutter` and `Dart` extensions installed. If not, do so. + +In this walkthrough, +we are going to use Visual Studio Code. +We will assume you have this IDE installed, +as well as the `Flutter` and `Dart` extensions installed. +If not, do so. extensions -After restarting Visual Studio Code, let's create a new project! -Click on `View > Command Palette`, type `Flutter` and click on -`Flutter: New Project`. It will ask you for a name of the new project -- just type something like 'demo_app' - and then click `Enter` and your -project should start setting up! +After restarting Visual Studio Code, +let's create a new project! +Click on `View > Command Palette`, +type `Flutter` +and click on +`Flutter: New Project`. +It will ask you for a name of the new project - +just type something like "demo_app" - +and then click `Enter` +and your project should start setting up! Let's run our newly created app. -On the bottom menu of Visual Studio Code, click on the device button -and you are shown a menu asking you to choose a device you want to run -the app from. I'll be going with iPhone 14 Pro Max. +On the bottom menu of Visual Studio Code, +click on the device button +and you are shown a menu +asking you to choose a device you want to run the app from. +I'll be going with iPhone 14 Pro Max. Bottom menu | After clicking, you are prompted with this menu :-------------------------:|:-------------------------: ![](https://user-images.githubusercontent.com/17494745/200813538-ceb06084-95ed-492f-940e-27ceaf86c6da.png) | ![](https://user-images.githubusercontent.com/17494745/200813745-5c75d190-5306-4f7c-88da-cffea66d4a27.png) -After setting up the device, the emulator should be shown. -After that, in Visual Studio Code, click on `Run > Start debugging`. -The build process will start and, after it is finished, +After setting up the device, +the emulator should be shown. +After that, +in Visual Studio Code, +click on `Run > Start debugging`. +The build process will start and, +after it is finished, the app will start on the newly created emulator. You should now see an "Hello World" app running. -Awesome! :tada: +Awesome! πŸŽ‰ hello_world ## 1. Project structure + We could implement a really simple project structure for this demo. -But, just for learning purposes, let's implement a structure that is -divided into four layers: +But, just for learning purposes, +let's implement a structure +that is divided into four layers: - [**presentation**](https://codewithandrea.com/articles/flutter-presentation-layer/): consisting of widgets, states (either local or shared) and controllers. @@ -1863,18 +2007,22 @@ our data sources and repositories. We will interact with APIs here. > This structure borrows many concepts from [DDD (Domain-driven-design)](https://en.wikipedia.org/wiki/Domain-driven_design), -where the codebase is modeled and implemented according -to domain logic and concepts. +where the codebase is modeled +and implemented according to domain logic and concepts. -We will simplify these four layers because it is a small project. -But if you were to work in a corporate environmnent, you would be dealing +We will simplify these four layers +because it is a small project. +But if you were to work in a corporate environmnent, +you would be dealing with various APIs, data sources and a large amount of models. This structure makes it much easier to maintain code at a larger scale. -Although we might be breaking the [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it) -principle here, this is just to show how to structure your code +Although we might be breaking \ +the [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it) principle here, +this is just to show how to structure your code in a maintainable manner. -Let's start! Firstly create the following folder structure. +Let's start! +Firstly create the following folder structure. ``` lib @@ -1887,7 +2035,8 @@ lib main.dart ``` -In the `lib/models/todo.dart` file, add the following piece of code. +In the `lib/models/todo.dart` file, +add the following piece of code. ```dart class Todo { @@ -1914,16 +2063,19 @@ class Todo { } ``` -This should be nothing new to you. We declared -each member field and added a function that -parses a JSON object to the class. +This should be nothing new to you. +We declared each member field +and added a function +that parses a JSON object to the class. -Next up, let's head to the repository file. Firstly, -run `flutter pub add http` +Next up, +let's head to the repository file. +Firstly, run `flutter pub add http` to install the `http` package, as we are going to need it to fetch data from a third-party API. -After that, let's create the `lib/repository/todoRepository.dart` file. +After that, +let's create the `lib/repository/todoRepository.dart` file. ```dart import 'dart:convert'; @@ -1954,12 +2106,15 @@ class HTTPTodoRepository implements TodoRepository { } ``` -Here, we are creating an `abstract` class, which will serve -as an interface for creating the `HTTPTodoRepository` class. -The class, since implements the `TodoRepository` abstract class, +Here, we are creating an `abstract` class, +which will serve as an interface for creating the `HTTPTodoRepository` class. +The class, +since implements the `TodoRepository` abstract class, will have to implement the `getTodos()` function. -In this function, we will call an API which returns an array of todos. -In case the call is successful, we parse each decoded json object +In this function, +we will call an API which returns an array of todos. +In case the call is successful, +we parse each decoded json object and convert it to a `Todo` object. Now let's go and implement the `lib/services/todoService.dart` file. @@ -1979,23 +2134,33 @@ class TodoService { } ``` -In this class, we initialize it by creating a `TodoRepository`. +In this class, +we initialize it by creating a `TodoRepository`. We use this field member in the `getTodos()` function, -which in turn, calls the `TodoRepository's` function to fetch -the todos list. +which in turn, +calls the `TodoRepository's` function to fetch the todos list. -You might be asking yourself: "Well, mate, that's a lot of work +You might be asking yourself: +"Well, mate, that's a lot of work for just a simple fetching function, isn't it?". -Well, in this case, you'd be right. But we're just learning a -maintainable way of structuring our code. -This service might (and *is*, in this case) redudant. -But imagine if we have widgets that necessitate objects +Well, in this case, you'd be right. +But we're just learning a maintainable way of structuring our code. +This service might +(and *is*, in this case) +redudant. +But imagine if we have widgets +that necessitate objects that stem from various data sources. -It will be *the service's just* to fetch whatever data is needed -from each repository, compile it and give it to the widget. - -Let's continue. In the `main.dart` file, let's fetch the todo list -and show the first one, just to check that everything works. +It will be *the service's job* +to fetch whatever data is needed from each repository, +compile it +and give it to the widget. + +Let's continue. +In the `main.dart` file, +let's fetch the todo list +and show the first one, +just to check that everything works. Import the service and the models. ```dart @@ -2054,21 +2219,27 @@ class _MyHomePageState extends State { If you re-run the app, you should see something like this. It is displaying the first todo title from the fetched list. -We used the `FutureBuilder` class to indicate -that the data residing within will come at a later stage - -the todo list. If the data comes, we show the first todo title. -And we did all this in a `StatefulWidget`, with the `State`. - -In the `_MyHomePageState` class, we declared that a -`Future` todos list is expected and fetched it in the -`initState()` method - it only runs one time, which is exactly what we want. +We used the `FutureBuilder` class +to indicate that the data residing within will come at a later stage - +the todo list. +If the data comes, +we show the first todo title. +And we did all this in a `StatefulWidget`, +with the `State`. + +In the `_MyHomePageState` class, +we declared that a `Future` todos list is expected +and is later fetched in the `initState()` method - +it only runs one time, which is exactly what we want. Hurray, we just set up all the data we need! -Now it's just about making it pretty :sparkles:. +Now it's just about making +our app look pretty ✨. ## 2. Creating a list of todos + Let's create a new widget to encapsulate our todo list. In Visual Studio Code, at the end of the `main.dart` file, click `Enter` a few times and type `stful`. @@ -2107,23 +2278,28 @@ class TodoList extends StatelessWidget { ``` This new stateless widget receives a `todoList` as argument. -This widget will return a `ListView` widget, which has a `itemBuilder` -property that will render a list of items. +This widget will return a `ListView` widget, +which has a `itemBuilder` property +that will render a list of items. -In the `itemCount` property, we will tell how many items we want -the list to show. In this case, we want the length of the todo list. +In the `itemCount` property, +we will tell how many items we want the list to show. +In this case, we want the length of the todo list. -In the `padding` property, we will add an +In the `padding` property, +we will add an [`EdgeInsets.all()`](https://api.flutter.dev/flutter/painting/EdgeInsets-class.html) -spacing. This will add a spacing of `16.0` on all directions (up, right, left, down). +spacing widget. +This will add a spacing of `16.0` on all directions (up, right, left, down). -In the `itemBuilder` property, we get access to the `context` and `index` -of the rendered component. We are adding a `Divider` in between -every item. So, the `i` value *includes* the `Divider` components as well. +In the `itemBuilder` property, +we get access to the `context` and `index` of the rendered component. +We are adding a `Divider` in between every item. +So, the `i` value *includes* the `Divider` components as well. Therefore, to correctly fetch the index of the item in the list, -we will use the ListView index and use the -[`~/`](https://api.flutter.dev/flutter/dart-core/double/operator_truncate_divide.html) -operator. This will yield integer part of a division. +we will use the ListView index +and use the [`~/`](https://api.flutter.dev/flutter/dart-core/double/operator_truncate_divide.html) operator. +This will yield integer part of a division. For example, `1 2 3 4 5` will be `0 1 1 2 2`. Now, let's use this new widget and change the `_MyHomePageState`, @@ -2142,20 +2318,23 @@ You should now be able to scroll the list, like so! list ## 3. Adding interactivity -We want to be able to click on a todo item and -mark it as "completed". To do this, we ought to add -interactivity to our `TodoList`. + +We want to be able to click on a todo item +and mark it as "completed". +To do this, we ought to add interactivity to our `TodoList`. To do this, we got to convert our stateless widget into a *stateful widget*. Doing this is fairly simple with Visual Studio Code. -Simply double-click on `TodoList`, a yellow lightbulb -will appear to the left side. Simply click it and -click in `Convert to Stateful Widget`. +Simply double-click on `TodoList` +and a yellow lightbulb will appear to the left side. +Simply click it +and click in `Convert to Stateful Widget`. lightbuld -This will effectively create a new `State` to the -widget and add it. You should now have the following code: +This will effectively create a new `State` +to the widget and add to it. +You should now have the following code: ```dart class TodoList extends StatefulWidget { @@ -2189,9 +2368,10 @@ class _TodoListState extends State { } ``` -You now have the `TodoList` and `_TodoListState`, -which refers to the state of the former. Notice it -is preceded with an underscore. This enforces privacy +You now have the `TodoList` +and `_TodoListState`, which refers to the state of the former. +Notice it is preceded with an underscore. +This enforces privacy and is best practice for `State` objects and private fields. Let's change the widget to look like the following: @@ -2249,21 +2429,24 @@ class _TodoListState extends State { } ``` -Let's break it down. The `State` object (`_TodoListState`) -now has a `_doneList` set. This set -(a set is like a list but guarantees each object is unique) -, as the underscore symbol entails, is private. +Let's break it down. +The `State` object (`_TodoListState`) now has a `_doneList` set. +This set (a set is like a list but guarantees each object is unique), +as the underscore symbol entails, +is private. This list will hold *the list of todos marked as **done***. -Inside the `ListView.builder()` widget, we have changed -the `itemBuilder`. We have added the following line: +Inside the `ListView.builder()` widget, +we have changed the `itemBuilder`. +We have added the following line: ```dart final completed = _doneList.contains(todoObj); ``` We are checking the item is in the `_doneList` set. -If so, we will add a strikethrough effect on the text to symbolize this. +If so, +we will add a strikethrough effect on the text to symbolize this. ```dart title: Text( @@ -2276,12 +2459,14 @@ If so, we will add a strikethrough effect on the text to symbolize this. ), ``` -Now, the only thing that is left is to mark a todo item -as *complete* or *incomplete* by tapping it. -Inside the `ListTile`, we add an `onTap` property, +Now, the only thing that is left to do +is to mark a todo item as *complete* or *incomplete* by tapping it. +Inside the `ListTile`, +let's add an `onTap` property, which is called everytime the list item is tapped, and change the state accordingly. -If the item is completed, we mark it as incomplete, and vice-versa. +If the item is completed, +we mark it as incomplete, and vice-versa. ```dart onTap: (() { @@ -2295,25 +2480,32 @@ If the item is completed, we mark it as incomplete, and vice-versa. }), ``` -Now, if you open your app, you can scroll and check items -and set them as `done` and reverse that action. Great job! +Now, if you open your app, +you can scroll and check items +and set them as `done` and reverse that action. +Great job! ![interactivity](https://user-images.githubusercontent.com/17494745/200861445-b4550a49-98cc-4f80-ba02-6ceff7fa17da.gif) ## 4. Adding navigation -We have added a stateful widget and are keeping track of what -todos are marked as `completed` or not. It would be great to -actually have a page where we see this list of completed items. + +We have added a stateful widget +and are keeping track of what todos are marked as `completed` or not. +It would be great to actually have a page +where we see this list of completed items. Currently, our widget tree looks like this. `MyApp` -> `MyHomePage` (which has the `todoList` as local state) -> `TodoList` (which has the `doneList` as local state). -We need to merge `MyHomePage` and `TodoList` into a single -widget with having the `todoList` and `doneList` to be able to -add navigation. Mergint these two in one will lead to a new -`TodoList` widget, that will look like this. +We need to merge `MyHomePage` and `TodoList` +into a single widget +that has the `todoList` and `doneList` fields +so we are be able to add navigation. +Merging these two in one +will lead to a new `TodoList` widget, +that will look like this. ```dart class TodoList extends StatefulWidget { @@ -2391,12 +2583,13 @@ class _TodoListState extends State { ``` Nothing was fundamentally changed. -We wrapped the `TodoList` with the same widgets of -the `MyHomePage` widget. We also changed the `AppBar.title` +We wrapped the `TodoList` +with the same widgets of the `MyHomePage` widget. +We also changed the `AppBar.title` to `Text('todo item list')`. -We now also need to change the `MyApp` to call this -newly edited widget. +We now also need to change the `MyApp` +to call this newly edited widget. ```dart class MyApp extends StatelessWidget { @@ -2412,11 +2605,13 @@ class MyApp extends StatelessWidget { } ``` -If you run the application, it looks the same as before. +If you run the application, +it looks the same as before. The only difference now is that we have all the state in the same widget (`TodoList`). -Inside the `_TodoListState` widget state class, let's add a button in the app bar +Inside the `_TodoListState` widget state class, +let's add a button in the app bar to navigate to the new page. ```dart @@ -2435,9 +2630,10 @@ to navigate to the new page. ), ``` -Let's implement the `_pushCompleted` function, that is executed -everytime the icon button is clicked on the appbar. -We want to navigate to the page that shows the completed todo items. +Let's implement the `_pushCompleted` function, +that is executed everytime the icon button is clicked on the appbar. +We want to navigate to the page +that shows the completed todo items. Add the following function in `_TodoListState`. ```dart @@ -2474,25 +2670,32 @@ Add the following function in `_TodoListState`. } ``` -Let's break this down. We use the `Navigator` to push a new screen to -the stack. We pass the widget's `context` and then use the `push()` function -to add the screen to the stack. -In this case, we are pushing a `MaterialPageRoute`, inside the `builder` -property, we return a `Scaffold` object with an `appBar` and a `body`. -Inside this `body`, we are rendering a `ListView` with each todo item -inside the `_doneList` set. +Let's break this down. +We use the `Navigator` to push a new screen to the stack. +We pass the widget's `context` +and then use the `push()` function to add the screen to the stack. +In this case, +we are pushing a `MaterialPageRoute`. +Inside the `builder` property, +we return a `Scaffold` object with an `appBar` and a `body`. +Inside this `body`, +we are rendering a `ListView` with each todo item inside the `_doneList` set. -Since we are using `MaterialPageRoute` and `Scaffold`, the back button is automatically added -to the appbar, making it possible to *pop* the screen and go back to the -screen showing the todo list. +Since we are using `MaterialPageRoute` and `Scaffold`, +the back button is automatically added to the appbar, +making it possible to *pop* the screen +and go back to the screen showing the todo list. -If we rerun our app, we can now navigate between pages. Hurray! :tada: +If we rerun our app, +we can now navigate between pages. +Hurray! πŸŽ‰ ![navigation](https://user-images.githubusercontent.com/17494745/200880357-314bb388-5c0c-4955-ac22-f9ec59e418a6.gif) ## 5. Finishing touches -We can quickly custmize the theme of the app, and it's title. -Let's change the colors and give our fancy app a new title. + +We can quickly customize the theme of the app and its title. +Let's change the colors and give our fancy app a new one. ```dart @override @@ -2510,40 +2713,47 @@ Let's change the colors and give our fancy app a new title. } ``` -Your app should look like this, now! -You can choose the colors you like. Go creative! :tada: +Your app should look like this now! +You can choose the colors you like. +Go creative! final ## 6. Testing! -We have our app running. In fact, we should -have used a [TDD](https://github.com/dwyl/learn-tdd) -approach to get our app running. -The reason we didn't do this is to show you how some -code needs to be laid out to be *testable*. -As you previously seen, mocking objects in Flutter -works through **dependency injection**. -That is, these are functions receive the dependencies that -they depend on through, for example, their constructor. +We have our app running. +In fact, we should have used a [TDD](https://github.com/dwyl/learn-tdd) +approach whilst developing our app. +The reason we didn't do this +is to show you how some code needs to be laid out +to be *testable*. -For simplicity sake, we are not going to be using -any libraries like `get_it` or `Riverpod` to do -deeply nested dependency injection. -In our demo app, we only have two levels deep, +As you have previously seen, +mocking objects in Flutter +works through **dependency injection**. +That is, these are functions receive the dependencies +that they depend on through, for example, their constructor. + +For simplicity sake, +we are not going to be using any libraries like `get_it` or `Riverpod` +to do deeply nested dependency injection. +In our demo app, +we only have two levels deep, so mocking and testing is very simple. Let's start testing! ### 6.1 Unit testing -Let's start unit testing our `TodoRepository` -and `TodoService`. As it stands, both of these files -are not "testable". We ought to find a way to -mock the `http` requests. How do we do that? -Exactly. *Dependency injection*. - -But first, we need to add the dependencies -in `pubspec.yaml`. + +Let's start unit testing our `TodoRepository` and `TodoService`. +As it stands, both of these files are not "testable". +We ought to find a way to mock the `http` requests. +How do we do that? +Exactly. +*Dependency injection*. + +But first, +we need to add the dependencies in `pubspec.yaml`. In the `dev_dependencies` section, add the following two lines of code. @@ -2583,7 +2793,8 @@ class HTTPTodoRepository implements TodoRepository { } ``` -Now, on to testing. Create a directory in `test/unit` +Now, on to testing. +Create a directory in `test/unit` and add a new file `todoRepository_test.dart`. ```dart @@ -2600,8 +2811,9 @@ void main() { We are going to use `mockito`'s `@GenerateMocks` annotation to generate a mock object for the `http.Client`, which is used inside the function. -We could do it manually but since we can get it generated -to ourselves automatically, let's do it. +We could do it manually +but since we can get it generated to ourselves automatically, +we should take advantage of this. Run the following command. @@ -2609,10 +2821,10 @@ Run the following command. flutter pub run build_runner build --delete-conflicting-outputs ``` -This will generate a `todoRepository_test.mocks.dart` file -with the generated mocks. -Import the file in the `todoRepository_test.dart` file and -let's create our first tests! +This will generate +a `todoRepository_test.mocks.dart` file with the generated mocks. +Import the file in the `todoRepository_test.dart` file +and let's create our first tests! ```dart import 'todoRepository_test.mocks.dart'; @@ -2653,14 +2865,16 @@ void main() { ``` Let's break down how we are testing the repository. -We are creating a `final client = MockClient()` using -the generated `MockClient` from the `todoRepository_test.mocks.dart` -file. We are specifying that this client +We are creating a `final client = MockClient()` +using the generated `MockClient` +from the `todoRepository_test.mocks.dart` file. +We are specifying that this client will return an array with a single todo item. -Using this new `MockClient`, we replace the class `client` -with the `MockClient` and run the test. -The same procedure is done, except an exception -is expected to rise. +Using this new `MockClient`, +we replace the class `client` with the `MockClient` +and run the test. +The same procedure is done, +except an exception is expected to rise. Let's do the same process for the `TodoService.dart` file. We need to change it, like so. @@ -2677,8 +2891,8 @@ class TodoService { ``` -Create a new `todoService_test.dart` and add -the following lines of code. +Create a new `todoService_test.dart` +and add the following lines of code. ```dart import 'package:http/http.dart' as http; @@ -2697,9 +2911,9 @@ Run the following command. flutter pub run build_runner build --delete-conflicting-outputs ``` -This will generate a `todoService_test.mocks.dart` file -with the generated mocks. Similarly, we will use this -file for the tests in the same fashion as before. +This will generate a +`todoService_test.mocks.dart` file with the generated mocks. +Similarly, we will use this file for the tests in the same fashion as before. In `todoService_test.dart`, add the following code. ```dart @@ -2764,14 +2978,17 @@ All that's left is testing the widgets. Let's do it! ### 6.2 Widget testing -To test our widgets, we need to pass -the `TodoService` so we can mock it in our tests. -Normally we would use a Provider to do this but this -is a simple app, so there is no need to add complexity -and third-party libraries. -Let's do these changes. Head over to `lib/main.dart` -and change the `TodoList` class like so. +To test our widgets, +we need to pass the `TodoService` +so we can mock it in our tests. +Normally we would use a Provider to do this +but this is a simple app, +so there is no need to add complexity and third-party libraries. + +Let's do these changes. +Head over to `lib/main.dart` and change the `TodoList` class. +Like so. ```dart class TodoList extends StatefulWidget { @@ -2784,8 +3001,8 @@ class TodoList extends StatefulWidget { } ``` -Now, we need to change the `MyApp` class to pass -a `TodoService` instance to `TodoList`. +Now, we need to change the `MyApp` class +to pass a `TodoService` instance to `TodoList`. It should look like this, now. ```dart @@ -2808,8 +3025,8 @@ class MyApp extends StatelessWidget { ``` Now we can test these widgets! -Create a new directory `test/widget` and -create a file named `widget_test.dart`. +Create a new directory `test/widget` +and create a file named `widget_test.dart`. ```dart @@ -2852,19 +3069,20 @@ void main() { ``` We use the `testWidgets` function to test the widget. -In turn, we get a `tester` object which allows us -to perform actions. We initialize and create the -widget by using `await test.pumpWidget(const MyApp())`. +In turn, we get a `tester` object +which allows us to perform actions. +We initialize and create the widget by using `await test.pumpWidget(const MyApp())`. We then check if the app bar is rendered. -To do this, we use the `find` class to find -the widget by text and check if it was built in the widget tree. +To do this, we use the `find` class to find the widget by text +and check if it was built in the widget tree. We then use a `Matcher` to make the assertion. In this case, we check if we `findOneWidget`. -If we run `flutter test --coverage`, we will see this test should pass. +If we run `flutter test --coverage`, +we will see this test should pass. -Let's now add a test to check if the list is rendered -with a list of todos. Add the following test. +Let's now add a test to check if the list is rendered with a list of todos. +Add the following test. ```dart testWidgets('Check if item list is rendered', (WidgetTester tester) async { @@ -2885,25 +3103,28 @@ with a list of todos. Add the following test. }); ``` -In this test, we are instantiating a `MockTodoService`, +In this test, +we are instantiating a `MockTodoService`, specifying the return value of the `getTodos()` and then using it when creating a `TodoList` widget. -We can't create `TodoList` by itself because -it necessitates to be a child of `MaterialApp`. -Hence why we use `MediaQuery` with `MaterialApp` which in turn -creates a `TodoList` widget that we want to test. - -With `tester.pumpWidget()`, we instantiate the -widget. This won't suffice, though. -The widget needs to render any animations and -run `initState` to fetch the todos item. +We can't create `TodoList` by itself +because it needs to be a child of `MaterialApp`. +Hence why we use `MediaQuery` with `MaterialApp` which, +in turn, creates a `TodoList` widget that we want to test. + +With `tester.pumpWidget()`, +we instantiate the widget. +This won't suffice, though. +The widget needs to render any animations +and run `initState` to fetch the todos item. For this, we add `await tester.pump()` with a specified duration. -This schedules a frame and triggers a rebuild of the widget, +This schedules a frame +and triggers a rebuild of the widget, running the clock by that amount. We only need `100 ms` in our case. -After this, we assert if the rendered list -contains a todo item with a title "mocktitle". +After this, +we assert if the rendered list contains a todo item with a title "mocktitle". Let's add another test. @@ -2933,19 +3154,22 @@ Let's add another test. }); ``` -In this test, we are rendering the `TodoList`, +In this test, +we are rendering the `TodoList`, tapping on a todo item (thus marking it as `complete`) and then navigating to the done todo item list. -For this, we use `tester.tap(find.byIcon((Icons.list)))` +For this, +we use `tester.tap(find.byIcon((Icons.list)))` to find the button and tap it. -We then use `tester.pumpAndSettle()`, which essentially -waits for all the animations to complete. -After this, we check if the done list screen is rendered -in the widget tree. - -We can keep adding tests to cover the rest of -the scenarios. Copy the following code and replace -your existing tests to cover all edge cases. +We then use `tester.pumpAndSettle()`, +which essentially waits for all the animations to complete. +After this, +we check if the done list screen is rendered in the widget tree. + +We can keep adding tests +to cover the rest of the scenarios. +Copy the following code +and replace your existing tests to cover all edge cases. Your `main` function should now look like this. ```dart @@ -3104,11 +3328,12 @@ void main() { ``` The final changes we ought to do is in the `main.dart` file. -We can't directly test the `main()` function that -runs the application. +We can't directly test the `main()` function +that runs the application. So, in order to get a real coverage report, add the following lines around the function. -This way, when testing, the compiler skips this function, +This way, when testing, +the compiler skips this function, as it is not needed to be tested. ```dart @@ -3120,9 +3345,12 @@ void main() { ``` ### 6.3 Test coverage -To get the test coverage, we are going to simply run -three commands. However, firstly, if you are on MacOS, -you need to install `lcov`. For this, run the following command + +To get the test coverage, +we are going to simply run three commands. +However, firstly, if you are on MacOS, +you need to install `lcov`. +For this, run the following command to install it in your computer. ```sh @@ -3140,30 +3368,35 @@ genhtml coverage/lcov.info -o coverage/html open coverage/html/index.html ``` -The generated HTML will create files inside -the `coverage/` folder. Add it to your -`.gitignore` file. +The generated HTML will create files +inside the `coverage/` folder. +Add it to your `.gitignore` file. Your browser should have opened a window, like so. image -Congratulations, you now have a fully tested -application! Awesome job! :tada: +Congratulations, +you now have a fully tested application! +Awesome job! πŸ‘ # Final remarks πŸ‘‹ + In this document (if you actually read it all the way through πŸ˜‰), -you went from 0 to hero with Flutter. You learnt important -principles and you *applied* them to create your own app in just -around 20 minutes! Give yourself a pat on the back! :tada: +you went from 0 to hero with Flutter. +You learnt important principles and *applied* them +to create your own app in just around 20 minutes! + +Give yourself a pat on the back! -If you wish to learn a bit more, take a look -at this repo's `guides` folder to -learn about [logging in with Firebase](./guides/login-firebase-tutorial.md) +If you wish to learn a bit more, +take a look at this repo's `guides` folder +to learn about [logging in with Firebase](./guides/login-firebase-tutorial.md) or [webviews in Flutter](./guides/webview-tutorial.md). -If you want to see more fully tested projects, check these out! +If you want to see more fully tested projects, +check these out! - [flutter-todo-list-tutorial](https://github.com/dwyl/flutter-todo-list-tutorial) - [flutter-counter-example](https://github.com/dwyl/flutter-counter-example) From 8c453fc2af1b1c619041d3c12bd1206057a33562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 13 Dec 2022 12:16:45 +0000 Subject: [PATCH 32/33] feat: Adding proper CI. #68 --- .github/dependabot.yml | 8 ++++++++ .github/workflows/ci.yml | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c164b65 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: mix + directory: "/" + schedule: + interval: daily + time: "12:00" + timezone: Europe/London \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7311f66 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: Build + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: macos-latest + defaults: + run: + working-directory: ./demo_app + + steps: + - uses: actions/checkout@v3 + + # Installing Flutter because it's easier to generate .lcov files for test coverage + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + version: '3.3.8' + channel: 'stable' + + - name: Install dependencies + run: flutter pub get + + # Your project will need to have tests in test/ and a dependency on + # package:test for this step to succeed. Note that Flutter projects will + # want to change this to 'flutter test'. + - name: Run tests + run: flutter test --coverage + + - uses: codecov/codecov-action@v2 + with: + files: coverage/lcov.info + verbose: true # optional (default = false) \ No newline at end of file From fb031a8ce5691132d770d771758072383dc4aa34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 13 Dec 2022 12:24:52 +0000 Subject: [PATCH 33/33] fix: Installing Flutter on CI using correct parameter. #68 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7311f66..7fd3bb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - name: Install Flutter uses: subosito/flutter-action@v2 with: - version: '3.3.8' + flutter-version: '3.3.8' channel: 'stable' - name: Install dependencies