├── LICENSE ├── README.md └── images └── guide └── android_studio_sdk_tools.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppBrewery Flutter : Null Safety Guide 2 | 3 | ## Read Before Starting 4 | 5 | - **DISCLAIMER : This repository is not an official outcome of [London AppBrewery Team](https://github.com/londonappbrewery)** 6 | 7 | - This repository is made to help new students get through [**AppBrewery Flutter Course**](https://www.udemy.com/course/flutter-bootcamp-with-dart/) 8 | 9 | - This repository contains notes for **only coding (project) sections** and explains what has changed and what's the difference. 10 | 11 | - If something is not covered here, **start a discussion**, not an issue! I will try to add it then. 12 | 13 | - Use latest versions of required **`packages`** and **`plugins`**, find them on [**pub.dev**](https://pub.dev/) 14 | 15 | ## Index 16 | 17 | #### 1. [Terminology](#terminology) 18 | 19 | #### 2. [Common issues and fixes](#common-issues-and-fixes) 20 | 21 | 1. #### [Using old Flutter SDK?](#1-using-old-flutter-sdk) 22 | 23 | 2. #### [Android license status unknown](#2-android-license-status-unknown) 24 | 25 | 3. #### [Option to create a new package is missing](#3-option-to-create-a-new-package-is-missing) 26 | 27 | 4. #### [Migrating V2](#4-migrating-v2) 28 | 29 | #### 3. [Resources](#resources) 30 | 31 | #### 4. [Code to Update](#code-to-update) 32 | 33 | 1. #### [Section 3 : I Am Rich App](#section-3--i-am-rich-app-lesson-28) 34 | 35 | 2. #### [Section 7 : Dicee App](#section-7--dicee-app-lesson-53) 36 | 37 | 3. #### [Section 9 : Xylophone App](#section-9--xylophone-app-lessons-76-77) 38 | 39 | 4. #### [Section 10 : Quizzler App](#section-10--quizzler-app-lesson-94) 40 | 41 | 5. #### [Section 12 : BMI Calculator App](#section-12--bmi-calculator-app-lessons-125-126-128-129) 42 | 43 | 6. #### [Section 13 : Clima App](#section-13--clima-app-lesson-140) 44 | 45 | 7. #### [Section 14 : Flash Chat App](#section-14--flash-chat-app-lessons-169-194) 46 | 47 | ## Terminology 48 | 49 | ##### [Go back to Index](#index) 50 | 51 | ### 1. Deprecated 52 | 53 | - You will often come across **`deprecated`** stuff, where it says **This is `deprecated`**. This means it's **not recommended** to use it anymore in your projects. You should avoid it and use alternatives. 54 | 55 | ### 2. Null Safety 56 | 57 | - **Null safety is not your enemy!** It's there you help you so you don't accidentally make something null and crash your app. 58 | 59 | - Dart has **`sound null safety`**. Basically, if you're writing any code that compiler thinks might end up being **`null`**, it will notify you right away! Isn't that cool? 60 | 61 | - Read more : [**Sound Null Safety**](https://dart.dev/null-safety), [**Understanding Null Safety**](https://dart.dev/null-safety/understanding-null-safety), [**Null Safety in Flutter**](https://flutter.dev/docs/null-safety) 62 | 63 | ## Common Issues and Fixes 64 | 65 | ##### [Go back to Index](#index) 66 | 67 | #### 1. Using old Flutter SDK? 68 | 69 | - Use latest **`Flutter SDK`**, currently I am using **`2.2`** in **`stable channel`** 70 | 71 | - To upgrade old one, run **`flutter upgrade`** in your **`Terminal / Command Prompt (cmd)`** 72 | 73 | #### 2. **`Android license status unknown`** 74 | 75 | - There are couple of things that can cause this, I'll keep adding them in future! For now I have these solutions, 76 | 77 | ##### Solution 1. Accept the new ones! 78 | 79 | - Just run **`flutter doctor --android-licenses`** 80 | 81 | - Normally, this does the job. If it doesn't, go ahead. 82 | 83 | ##### Solution 2. Install / Update SDK Command Line Tools 84 | 85 | - Open Settings panel by, 86 | 87 | - **`File > Settings`** (**Windows and Linux**) 88 | 89 | - **`Android Studio > Preferences`** (**Mac**) 90 | 91 | - Then navigate to, 92 | 93 | **`Appearance & Behavior > System Settings > Android SDK`** 94 | 95 | - Select the **`SDK Tools` Tab** 96 | 97 | - Select **`Android SDK Command Line Tools`** and click **`Apply`** 98 | 99 | - A dialog will pop up and ask you if you want to install these. 100 | 101 | Click Yes/OK and let it install, after that, close **`Android Studio`** and **`restart`** it. 102 | 103 | 104 | 105 | #### 3. Option to create a new **`package`** is missing 106 | 107 | - When I first encountered this issue, I thought there must be something wrong with just this particular update. 108 | 109 | - I searched it online, posted on Reddit, twitter, but found nothing. 110 | 111 | - Later on, I got to know that **`New > Package`** and **`New > Directory (Folder)`** options have now merged! 112 | 113 | - So, to create a new **`package`** or just a **`folder`**, simply use **`New > Directory`** option. 114 | 115 | #### 4. Migrating V2 116 | 117 | - As we know [**Sound Null Safety**](https://dart.dev/null-safety) was added to **Flutter 2**. 118 | 119 | - And before **Flutter 2** old apps don't have [**Sound Null Safety**](https://dart.dev/null-safety) feature. 120 | 121 | - So we need to fix this, and thank [**Flutter**](https://flutter.dev/) we have simple way to do. 122 | 123 | - To fix this issue easily , 124 | 125 | * Delete all files and folders in App folder **`except`** course materials **`lib` - `assets` - `fonts` - `pubspec.yaml` etc.** 126 | 127 | * For Example; 128 | 129 | ![ww](https://user-images.githubusercontent.com/84624853/151516481-b0eb6102-215c-4cf7-9774-fccffc2e9245.jpg) 130 | 131 | 132 | - And then go to **`Terminal`** while its in project folder and 133 | 134 | - **Write this line to `Terminal`** 135 | 136 | - `flutter create .` 137 | 138 | - For Example; 139 | 140 | ![crate](https://user-images.githubusercontent.com/84624853/151516510-ae00c14b-5d79-42fc-9801-3dc9c822bfe4.jpg) 141 | 142 | 143 | - **Then Flutter starts rebuilding application with migrated version of it. And Done!** 144 | 145 | 146 | ## Resources 147 | 148 | ##### [Go back to Index](#index) 149 | 150 | #### 1. Try out Null Safety on [DartPad](https://dartpad.dev/?null_safety=true) 151 | 152 | #### 2. Read Updated [Flutter Docs](https://flutter.dev/docs) 153 | 154 | #### 3. Watch and Follow [Flutter's Official Youtube Channel](https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw) 155 | - To learn more about Null Safety and staying updated in general. 156 | 157 | # Code to Update 158 | 159 | ## Section 3 : I Am Rich App (Lesson 28) 160 | 161 | ##### [Go back to Index](#index) 162 | 163 | - You right clicked on **`res`** folder but didn't find **`Image Asset`**? Don't worry Follow these steps, 164 | 165 | - Right click on **`android`** folder and a pop-up menu will open up. 166 | 167 | From that, select **`Flutter > Open Android Module in Android Studio`** 168 | 169 | **If this doesn't work for you then follow these steps**, 170 | 171 | **1.** Close current project by pressing **`File > Close Project`** 172 | 173 | **2.** Now you will have the first screen of Android Studio. 174 | 175 | **3.** Press **`Open an Existing Project`**, then **`Open File or Project`** dialog will open. 176 | 177 | **4.** Here, navigate to your **Flutter project** in which, you want to add **`Image Asset`** 178 | 179 | **5.** Expand that and you will find **`android`** folder. Select that and press **`OK`** 180 | 181 | - **Both ways** should open **Android Part** of your **Flutter Project** in **`Android Studio`**. 182 | 183 | - Now, at bottom right, if it's running any **`gradle`** processes, let it run. Don't interrupt! However, if you close it, it'll rebuild everything when you reopen it. So, no need to worry! 184 | 185 | - After that long build process completes, you can find **`Image Asset`** option when you click on **`res`** folder, **Yay**! 186 | 187 | - Add assets and again, **`File > Close Project`**, **`Open an Existing Project`** and this time, select your **Flutter Project** and continue! 188 | 189 | ## Section 7 : Dicee App (Lesson 53) 190 | 191 | ##### [Go back to Index](#index) 192 | 193 | - **`FlatButton`** is **`deprecated`**, so use **`TextButton`** instead. 194 | 195 | ## Section 9 : Xylophone App (Lessons 76, 77) 196 | 197 | ##### [Go back to Index](#index) 198 | 199 | - Getting a lengthy error when trying to use **`audioplayers` plugin**? 200 | 201 | - All you need to do is open **`android > build.gradle` (Project Level `gradle` file)** 202 | 203 | - Inside **`buildscript {}`**, you'll find **`ext.kotlin_version` (Line 2 in file)** 204 | 205 | - Replace whatever version it is with [**Latest Stable Kotlin Version**](https://kotlinlang.org/docs/releases.html#release-details) 206 | 207 | - As of **July 23, 2021** it is, **`ext.kotlin_version = '1.5.21'`** 208 | 209 | - Now, **re-install** the app. If it's already running, press **Stop** then press **Run (Play)** again. 210 | 211 | - **`FlatButton`** is **`deprecated`**, so use **`TextButton`** instead. 212 | 213 | - By the end, the implementation of your **`TextButton`** should look like this: 214 | 215 | ```dart 216 | @override 217 | Widget build(BuildContext context) { 218 | return Expanded( 219 | child: TextButton( 220 | style: ButtonStyle( 221 | backgroundColor: MaterialStateProperty.all(color), 222 | ), 223 | onPressed: () { 224 | playSound(soundNumber); 225 | }, 226 | ), 227 | ); 228 | } 229 | ``` 230 | 231 | - **`AudioCache`** is **`deprecated`**, so use **`AudioPlayer`** instead. 232 | - By the end, your solution to playing the audio should look like this: 233 | ```dart 234 | void playSound(int soundNumber) { 235 | final player = AudioPlayer(); 236 | player.setSource(AssetSource('note$soundNumber.wav')); 237 | } 238 | ``` 239 | 240 | - An example of a working project as of 16/07/2022 has been linked below: 241 | - [Link to repository](https://github.com/vpatel-dev/xylophone-flutter) 242 | 243 | ## Section 10 : Quizzler App (Lesson 94) 244 | 245 | ##### [Go back to Index](#index) 246 | 247 | - Due to **`null safety`**, all variables in a class must have a value assigned, when created. If not, they must be declared **`Nullable`** intentionally. This rule also applies to **`Stateless`** and **`Stateful`** widgets. On top of that, in classes extending **`StatelessWidget`**, all variables must be declared **`final`** 248 | 249 | - So, make your **`Question`** class like this, 250 | ```dart 251 | class Question { 252 | String questionText; 253 | bool questionAnswer; 254 | 255 | Question(this.questionText, this.questionAnswer); 256 | 257 | // If you want named parameters 258 | // Question({required this.questionText, required this.questionAnswer}); 259 | } 260 | ``` 261 | 262 | - **`@required`** is replaced by just **`required` (Without @ sign)** 263 | 264 | - Here, the **Keyword `this`** points to **current context**, which happens to be **`Question`** class. 265 | 266 | - **`FlatButton`** is **`deprecated`**, so use **`TextButton`** instead. 267 | 268 | ## Section 12 : BMI Calculator App (Lessons 125, 126, 128, 129) 269 | 270 | ##### [Go back to Index](#index) 271 | 272 | - **`@required`** is replaced by just **`required` (Without @ sign)** 273 | 274 | - So, while making **`ReusableCard`**, lesson shows you can skip using **`cardChild`** property, but that isn't possible, due to **`null safety`** 275 | 276 | - This part is tricky, because now you can't have null arguments anymore. 277 | 278 | - So, you must have to intentionally make it **`Nullable`**, by adding **`?`** to it, like this, 279 | ```dart 280 | class ReusableCard extends StatelessWidget { 281 | final Color colour; 282 | final Widget? cardChild; 283 | 284 | ReusableCard({required this.colour, this.cardChild}); 285 | 286 | @override 287 | Widget build(BuildContext context) { 288 | return Container( 289 | decoration: BoxDecoration( 290 | color: colour, 291 | ), 292 | child: child, 293 | ); 294 | } 295 | } 296 | ``` 297 | 298 | - Use it like **`ReusableCard(color: Colors.amber)`** and your app won't crash. 299 | 300 | - But, it's not same for **`IconContent`**, **`Icon`** can have **`null`** value, but **`Text`** can't! 301 | ```dart 302 | class IconContent extends StatelessWidget { 303 | final IconData? icon; 304 | final String? label; 305 | 306 | IconContent({this.icon, this.label}); 307 | 308 | @override 309 | Widget build(BuildContext context) { 310 | return Column( 311 | children: [ 312 | Icon(icon), 313 | Text(label ?? ''), 314 | ], 315 | ); 316 | } 317 | } 318 | ``` 319 | 320 | - So using **`??`** operator, you need to check if label is **`null`** or not, if it is, then you must provide a **`String`** value to it. Here, I provided an empty String. 321 | 322 | - Even if you don't pass any arguments like **`IconContent()`**, your app won't crash. 323 | 324 | - According to **Lesson 129**, **`ReusableCard`** now has a parameter named **`onPress`**, to get it working, use this, 325 | ```dart 326 | class ReusableCard extends StatelessWidget { 327 | final Color colour; 328 | final Widget? cardChild; 329 | final void Function()? onPress; 330 | 331 | ReusableCard({required this.colour, this.cardChild, this.onPress}); 332 | 333 | @override 334 | Widget build(BuildContext context) { 335 | return GestureDetector( 336 | onTap: onPress, 337 | child: Container( 338 | decoration: BoxDecoration( 339 | color: colour, 340 | ), 341 | child: child, 342 | ), 343 | ); 344 | } 345 | } 346 | ``` 347 | - Because **`GestureDetector`**'s **`onTap`** property wants **`void Function()?`** as argument. 348 | 349 | - In **Lesson 128**, **`_InputPageState`** has a new variable which haven't been initialized. As I already told you, you must initialize them or make them **`Nullable`**. 350 | ```dart 351 | class _InputPageState extends State { 352 | Gender? selectedGender; 353 | } 354 | ``` 355 | 356 | - Here, making it **`Nullable`** will do the job. Rest of the code will work perfectly fine. 357 | 358 | ## Section 13 : Clima App (Lesson 140) 359 | 360 | ##### [Go back to Index](#index) 361 | 362 | - When running this app on a **Physical Device** running on: 363 | 364 | - **`Android` :** you will need **Internet Permission** because the app sends a **`request`** to the OpenWeatherMap **`API`**. For this, open **`AndroidManifest.xml`** by navigating to, 365 | 366 | **`android > app > src > main > AndroidManifest.xml`** 367 | 368 | and add the following line, 369 | 370 | ```xml 371 | 372 | ``` 373 | 374 | **Keep the existing location permissions** and add this above/below them. 375 | Add it under **`manifest`** tag, like this, 376 | 377 | ```xml 378 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 390 | . 391 | . 392 | . 393 | 394 | 395 | ``` 396 | 397 | - **`iOS` :** 398 | 399 | - There is **no required Internet Permission**. 400 | - When launching the app, you will probably have a message asking you for **Local Network Permission**: it is **not required** either. 401 | - To run your app on an iOS physical device, make sure that you selected a **Development Team** (refer to **Section 4 - Lesson 32** of the course): 402 | 1. Open the Flutter project's Xcode target with open ios/Runner.xcworkspace 403 | 2. Select the 'Runner' project in the navigator then the 'Runner' target in the project settings 404 | 3. Make sure a 'Development Team' is selected under Signing & Capabilities > Team. You may need to: 405 | 1. Log in with your Apple ID in Xcode first 406 | 2. Ensure you have a valid unique Bundle ID (for example *"com.put-your-name-here.clima"*) 407 | 3. Register your device with your Apple Developer Account 408 | 4. Let Xcode automatically provision a profile for your app 409 | 4. Select your iOS physical device as the target and click the Run button 410 | - You may have several pop-up asking you that "codesign" wants access to your Apple Development Team's key. Accept by entering your password (it's your Mac session's password, not your Apple ID's password) 411 | - :exclamation: **ATTENTION:** if you don't activate Internet on your physical device, it is likely that you will see a pop-up on your screen telling you that **an Internet connection is required** to verify if the developer (you) is reliable, so you would need to activate your Internet connection 412 | 413 | - If your app cannot retrieve your current location on your **`iOS` physical device** it is probably because the [geolocator 8.2.1 flutter package](https://pub.dev/packages/geolocator) has been updated and you will need to apply the following changes: 414 | - As of now (June 2022), the geolocator package indicates to add both **`NSLocationWhenInUseUsageDescription`** and **`NSLocationAlwaysUsageDescription`** permissions to access **Location Service**. Since iOS 11, the **`NSLocationAlwaysUsageDescription`** property key is [deprecated](https://developer.apple.com/documentation/bundleresources/information_property_list/nslocationalwaysusagedescription). Use instead **only one** of those permissions: 415 | - the **`NSLocationAlwaysAndWhenInUseUsageDescription`** to enable the **Location Service** in foreground and background, 416 | - the **`NSLocationWhenInUseUsageDescription`** to enable the service in foreground only, as [recommended by Apple](https://developer.apple.com/documentation/corelocation/choosing_the_location_services_authorization_to_request). 417 | 418 | To do so, open **`Info.plist`** file by navigating to: 419 | 420 | **`ios > Runner > Info.plist`** 421 | 422 | and add the following line right under the `` tag (your can customise the message in the `` tag, it has to explain why your app needs to have access to that particular permission): 423 | 424 | ```xml 425 | 426 | NSLocationWhenInUseUsageDescription 427 | This app needs access to your location to provide weather data of your current location. 428 | 429 | ``` 430 | 431 | - In your code you will have to explicitly ask the user for permission to use the **Location Service**. For that, update the **`getCurrentLocation()`** method in the **`location.dart`** file: 432 | - Short version: 433 | ```dart 434 | Future getCurrentLocation() async { 435 | try { 436 | LocationPermission locationPermission = await Geolocator.requestPermission(); 437 | 438 | if (LocationPermission.whileInUse == locationPermission || LocationPermission.always == locationPermission) { 439 | Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.lowest); 440 | latitude = position.latitude; 441 | longitude = postion.longitude; 442 | } 443 | } catch (e) { 444 | print(e); 445 | } 446 | } 447 | ``` 448 | - Long but more complete version: 449 | ```dart 450 | Future getCurrentLocationCheckingPermissions() async { 451 | bool serviceEnabled; 452 | LocationPermission locationPermission; 453 | 454 | // Test if location services are enabled. 455 | serviceEnabled = await Geolocator.isLocationServiceEnabled(); 456 | if (!serviceEnabled) { 457 | // Location services are not enabled don't continue 458 | // accessing the position and request users of the 459 | // App to enable the location services. 460 | return Future.error( 461 | 'Location services are disabled. Please activate them.'); 462 | } else { 463 | locationPermission = await Geolocator.checkPermission(); 464 | if (LocationPermission.unableToDetermine == locationPermission) { 465 | return Future.error( 466 | 'Unable to determine if location permissions are enabled.'); 467 | } else if (LocationPermission.denied == locationPermission || 468 | LocationPermission.deniedForever == locationPermission) { 469 | print(Future.error('Location permissions are denied: ' + 470 | locationPermission.toString())); 471 | locationPermission = await Geolocator.requestPermission(); 472 | } 473 | 474 | if (LocationPermission.whileInUse == locationPermission || 475 | LocationPermission.always == locationPermission) { 476 | Position position = await Geolocator.getCurrentPosition( 477 | desiredAccuracy: LocationAccuracy.lowest); 478 | this.latitude = position.latitude; 479 | this.longitude = position.longitude; 480 | } 481 | } 482 | } 483 | ``` 484 | 485 | 486 | ## Section 14 : Flash Chat App (Lessons 169-194) 487 | 488 | ##### [Go back to Index](#index) 489 | 490 | ### Setting up Firebase 491 | 492 | Following the Appbrewery course, you should have logged in Firebase with your Google account, and created a Firebase project that will be linked with your Flash Chat Flutter project. 493 | 494 | #### Add Firebase to your Flutter project 495 | 496 | - To do so, in the course it is showed that you have to go to your Firebase project and **add a new application**, selecting an **`Android application`** if you plan to deploy your app on Android, and/or an **`iOS application`** if you plan to deploy on iOS. 497 | - As of now **(July 2022)**, it is now possible to directly add a **`Flutter application`** which will save you a lot of time in configuration :confetti_ball: , so choose that option instead, and follow the instructions that will be displayed: 498 | 499 | #### Installing Firebase CLI and connect to it 500 | 501 | - Depending on your Operating System (Windows, macOS or Linux), you will find the steps to do on the [documentation](https://firebase.google.com/docs/cli?authuser=0&hl=fr#install_the_firebase_cli). 502 | - After the installation is done, connect to it by executin the following command in a terminal: **`firebase login`**. 503 | 504 | #### Installing Flutter SDK and creating a Flutter project 505 | 506 | - If you have arrived this far in the course, you have already installed the Flutter SDK a long time ago! 507 | - The Flutter project has alo been created already, it's our Flash Chat Flutter project. 508 | 509 | 510 | #### Installing the FlutterFire CLI 511 | - Open a terminal and run the following command line: **`dart pub global activate flutterfire_cli`** (it doesn't matter in which directory you run this command) 512 | 513 | 514 | #### Executing the FlutterFire CLI 515 | 516 | - Before executing the **FlutterFire CLI**, make sure to change your **`Application ID`** for Android, and your **`iOS Bundle ID`** for iOS to make them **unique and personal** (if you have copied the Flash Chat Flutter project from the Appbrewery course, they are defined with their company ID which you have to change to make it your own): 517 | 518 | - **`Application ID`** for Android: 519 | - Open the **`build.gradle`** file located under **`android > app > build.gradle`** 520 | - Change the **`android > defaultConfig > applicationId`** property (the applicationId should be **co.appbrewery.flash_chat** --> change it for something unique and personal like **com.firstnamelastname.flash_chat**, or something you like that is unique and personal, or your domain name if you own one and wish to use it) 521 | 522 | - **`iOS Bundle ID`** for iOS: 523 | - Right-click on the "ios" folder and choose **`Flutter > Open iOS module in Xcode`** 524 | - Select "Runner" at the top of the left panel (the "Runner" with the blue icon), and in the center panel go to the **General tab**, then under it go to **`Identity > Bundle Identifier`** 525 | - You should find a Bundle Identifier like **co.appbrewery.flashChat** --> change it for something unique and personal like **com.firstnamelastname.flashChat** 526 | 527 | - After changing the **`Application ID`** for Android, and/or the **`iOS Bundle Identifier`** for iOS, open a terminal and go to the **ROOT FOLDER** of your Flutter Flash Chat project, then execute the command line given in the instructions: 528 | 529 | ```shell 530 | flutterfire configure --project=YOUR_FIREBASE_PROJECT_ID_HERE 531 | ``` 532 | (You can check what is your Firebase Project ID by either looking on your Firebase account in a browser, or by running the command line **`firebase projects:list`**) 533 | - After running the previous command, you should find in your Flutter project: 534 | 535 | - **`Flutter`**: your Firebase configuration file under **`lib > firebase_options.dart`** (the most important one, it contains both your Android and your iOS API keys to access Firebase services) 536 | 537 | - **`Android`**: your Firebase configuration file under **`android > app > google-services.json`** 538 | 539 | - **`iOS`**: a Firebase identifying file under **`ios > firebase_app_id_file.json`** (if the course is not updated, you might see that you should have a file called **`GoogleService-Info.plist`** instead under **`ios > Runner > GoogleService-Info.plist`** --> I am not an expert with Firebase, but my guess is that the **`GoogleService-Info.plist`** comes up when you configure your Firebase project by adding an iOS application instead of a Flutter application) 540 | 541 | 542 | #### Initialising Firebase 543 | 544 | - To initialise Firebase, start by adding to your Flutter project the **`firebase_core`** plugin: 545 | ```shell 546 | flutter pub add firebase_core 547 | ``` 548 | - Make sure that the Firebase configuration of your Flutter application is updated, by running the following command **in the root folder of your Flutter project directory**: 549 | ```shell 550 | flutterfire configure 551 | ``` 552 | - Then, if everything's fine, in your **`lib/main.dart`** file, change the main method to use the Firebase initialising method **`Firebase.initializeApp()`** 553 | ```dart 554 | import 'package:firebase_core/firebase_core.dart'; 555 | import 'package:flash_chat/firebase_options.dart'; 556 | 557 | 558 | Future main() async { 559 | WidgetsFlutterBinding.ensureInitialized(); 560 | 561 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 562 | 563 | runApp(FlashChat()); 564 | } 565 | ``` 566 | :exclamation::exclamation: It is **FUNDAMENTAL** that you add the parameter **`options: DefaultFirebaseOptions.currentPlatform`** because it will specify the configuration that will be used depending on the platform your application is running on (Android, iOS, macOS, Windows or Linux) --> feel free to explore the code of **`DefaultFirebaseOptions.currentPlatform`**, you will notice that it corresponds to your **`lib > firebase_options.dart`**, and that it simply verifies the platorm on which your application is running, then returns the appropriate configuration. 567 | 568 | - Finally, stop your application if it was running (to make a fresh start), and try to run it on Android and on iOS to verify that everything works on both platform (try to run it as well on macOS, Windows and/or Linux if you are developing for those platforms). 569 | 570 | If you encounter some errors, please have a look below to find some fixes that may be of help: 571 | 572 | - **`On Android`:** 573 | 574 | - The **`firebase_core`** plugin (as well as all [Firebase plugins](https://firebase.google.com/docs/flutter/setup?authuser=0&hl=fr&platform=ios#available-plugins)) requires at least the **`Android SDK version 31`** now (July 2022), so make the modification if needed under **`android > app > build.gradle`**: 575 | 576 | ```gradle 577 | android { 578 | compileSdkVersion 31 579 | ... 580 | } 581 | ``` 582 | 583 | - **`On iOS`:** 584 | 585 | - There was no problem encountered on my side. Please add more details about yours in the **Discussions** section if you have troubles here. 586 | 587 | 588 | #### Adding Firebase Plugins 589 | 590 | - Add the **`firebase_auth`** and the **`cloud_firestore`** plugins in your Flutter project: 591 | ```shell 592 | flutter pub add firebase_auth 593 | flutter pub add cloud_firestore 594 | ``` 595 | Click on **`Pub Get`** to make sure that you get the dependencies in your project. 596 | 597 | 598 | - **`On Android`**: 599 | 600 | - You may need to upgrade the **`minSdkVersion`** of the Android part of your Flutter project, under **`android > app > build.gradle`**, because some Firebase plugins have a minimal requirement of the SDK version 21 now (July 2022), like the **`cloud_firestore`** plugin for instance, so apply that change: 601 | ```gradle 602 | android { 603 | ... 604 | defaultConfig { 605 | ... 606 | minSdkVersion 21 607 | ... 608 | } 609 | ... 610 | } 611 | ``` 612 | 613 | - Those plugins require as well the **`Android SDK version 31`**, so make sure that you have it updated in your **`android > app > build.gradle`**: 614 | 615 | ```gradle 616 | android { 617 | compileSdkVersion 31 618 | ... 619 | } 620 | ``` 621 | 622 | 623 | - After adding the dependencies in the AndroidManifest.xml file and the build.gradle files, you might get this error: 624 | ```shell 625 | ERROR:D8: Cannot fit requested classes in a single dex file (# methods: 104246 > 65536) 626 | com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: 627 | The number of method references in a .dex file cannot exceed 64K. 628 | ... 629 | * What went wrong: 630 | Execution failed for task ':app:mergeExtDexDebug'. 631 | > A failure occurred while executing com.android.build.gradle.internal.tasks.DexMergingTaskDelegate 632 | > There was a failure while executing work items 633 | > A failure occurred while executing com.android.build.gradle.internal.tasks.DexMergingWorkAction 634 | > com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: 635 | The number of method references in a .dex file cannot exceed 64K. 636 | Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html 637 | ``` 638 | 639 | To fix this issue, inside the app-level build.gradle file, **`../android/app/build.gradle`**, 640 | the *minSdkVersion* property under the *defaultConfig* block in the *android* block should be set to **`21`** to avoid error. 641 | This is because multiDex support is enabled by default for sdkVersion 21. 642 | 643 | - Additional dependencies have to be added to the app-level build.gradle file, **`../android/app/build.gradle`**: 644 | ``` 645 | dependencies { 646 | ... 647 | implementation platform('com.google.firebase:firebase-bom:30.0.1') 648 | implementation 'com.google.firebase:firebase-auth' 649 | implementation 'com.google.firebase:firebase-firestore' 650 | ... 651 | } 652 | ``` 653 | 654 | - To enable internet connectivity on a physical device, add `` to the AndroidManifest.xml file. 655 | 656 | 657 | 658 | - **`On iOS`** : 659 | 660 | - Before running the application to check if everything is fine, update **`CocoaPods`**: 661 | ```shell 662 | pod repo update 663 | sudo gem install cocoapods 664 | pod setup 665 | ``` 666 | - Run your app to check if it's working: 667 | - If you encounter the following error: 668 | ```shell 669 | Error output from Cocoapods: 670 | [!] Automatically assigning platform 'iOS' with version '9.0' on target 'Runner' because no platform was specified. Please specify a platform for this target in your Podfile.android 671 | Error running pod install 672 | Error launching application on iPhone. 673 | ``` 674 | update your **`Podfile`** file under **`ios > Podfile`** by uncommenting the "platform" line and changing the version from 9.0 to 10.0 --> this specifies the minimum OS version that you are going to support for the pod project: 675 | ``` 676 | # Uncomment this line to define a global platform for your project 677 | platform :ios, '10.0' 678 | ``` 679 | then try to run your app again (it might take a long time, it took me around 30 minutes to make all the cocoapods installation). 680 | You should see in the "Run tab" the information **"Running pod install..."**: Flutter is initiating that to be able to install all of the Firebase plugin packages (firebase_core, firebase_auth and cloud_firestore) as cocoapods to our iOS app. 681 | 682 | 683 | 684 | 685 | 686 | ### Code Part 687 | 688 | 689 | - In **chat_screen.dart** file, the object type for the *loggedInUser* was previously **`FirebaseUser`**, and should now be replaced with **`User`**. Moreover, the Firestore instance is now retrieved with `FirebaseFirestore.instance` instead of `Firestore.instance`: 690 | 691 | ```dart 692 | class _ChatScreenState extends State { 693 | final _firestore = FirebaseFirestore.instance; 694 | final _auth = FirebaseAuth.instance; 695 | User loggedInUser; 696 | String message; 697 | ... 698 | } 699 | 700 | ``` 701 | 702 | 703 | - When retrieving the messages from your Firestore Database, you will notice some changes in the API: 704 | 705 | - The `getDocuments` method has been renamed to `get`: 706 | ```dart 707 | _firestore.collection('messages').getDocuments(); // <-- BEFORE 708 | 709 | _firestore.collection('messages').get(); // <-- NOW 710 | ``` 711 | 712 | - The **QuerySnapshot** property, **documents**, has been renamed to **docs**. 713 | ```dart 714 | void getMessages() async { 715 | final messages = await _firestore.collection('messages').get(); 716 | 717 | for (var message in messages.documents) { // <-- BEFORE 718 | print(message.data()); 719 | } 720 | 721 | for (var message in messages.docs) { // <-- NOW 722 | print(message.data()); 723 | } 724 | } 725 | ``` 726 | 727 | - When using a `StreamBuilder`, it is better to give it the type of data that will stream through, as it will greatly help you when manipulating the `AsyncSnapshot` and the data it contains (`snapshot.data`) - you can find the type of data by looking at the return type of the Firestore snapshots method (`_firestore.collection('messages').snapshots()`): 728 | ```dart 729 | StreamBuilder>>( // <-- By adding the type >... 730 | stream: _firestore.collection('messages').snapshots(), 731 | builder: (BuildContext context, AsyncSnapshot snapshot) { 732 | 733 | final messages = snapshot.data.docs; // <-- ... you will be able to access the 'docs' property because we manipulate a Stream of QuerySnapshot 734 | 735 | for (var message in messages) { 736 | final messageText = message.data()['text']; // <-- ... you will be able to access the Map with `message.data()` because we manipulate a Stream of QuerySnapshot of Map 737 | final sender = message.data()['sender']; 738 | ... 739 | } 740 | ... 741 | }, 742 | ); 743 | ``` 744 | 745 | 746 | 747 | 748 | - To deal with the case where we would have no data in our `StreamBuilder`, we do add a `CircularProgressIndicator` after checking the value of `!snapshot.hasData`. To center it on the screen, add a margin using `MediaQuery` to retrieve the height of the screen (if you want to see how it looks like, you can simply temporarily change the if condition with `true`): 749 | ```dart 750 | StreamBuilder>>( 751 | stream: _firestore.collection('messages').snapshots(), 752 | builder: (context, snapshot) { 753 | 754 | if (!snapshot.hasData) { // <-- change it temporarily to 'if (true)' to quickly see the result 755 | return Container( 756 | margin: 757 | EdgeInsets.only(top: MediaQuery.of(context).size.height / 3), 758 | child: CircularProgressIndicator( 759 | backgroundColor: Colors.lightBlueAccent, 760 | ), 761 | ); 762 | } 763 | ... 764 | }, 765 | ); 766 | ``` 767 | :exclamation::information_source: To avoid your `CircularProgressIndicator` to be stretched horizontally and look weird, make sure that the column in which your `MessagesStream` is into doesn't have the property **`crossAxisAlignment: CrossAxisAlignment.stretch`**: 768 | ```dart 769 | body: SafeArea( 770 | child: Column( 771 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 772 | // crossAxisAlignment: CrossAxisAlignment.stretch, // <-- remove this if you have it 773 | children: [ 774 | MessagesStream(), 775 | Container(...), 776 | ... 777 | ``` 778 | 779 | 780 | - The sorting of the messages in the chat screen is still chaotic even after the reversal in ***lesson 191***. 781 | To fix this problem, we need to add a **timestamp** field to the messages and sort the collection based on it as shown here: 782 | ```dart 783 | ... 784 | TextButton( 785 | onPressed: () { 786 | controller.clear(); 787 | _firestore.collection('messages').add({ 788 | 'text': messageText, 789 | 'sender': loggedInUser.email, 790 | 'timestamp': FieldValue.serverTimestamp(), // Here is the **timestamp** field. 791 | }); 792 | }, 793 | ... 794 | ``` 795 | 796 | We use the server time instead of generating a timestamp with the user device because: 797 | - Our users may be in different timezones so the time differences will affect our app. 798 | - Some devices could be set to incorrect times. 799 | 800 | ```dart 801 | class MessagesStream extends StatelessWidget { 802 | 803 | @override 804 | Widget build(BuildContext context) { 805 | return StreamBuilder( 806 | stream: _firestore.collection('messages').orderBy('timestamp').snapshots(), // Here, the **.orderBy** sorts the messages according to the server timestamps. 807 | builder: (context, snapshot) { 808 | ... 809 | 810 | ``` 811 | 812 | 813 | ### More Configuration on Firebase 814 | 815 | #### Cloud Firestore Authorisation and Security Rules 816 | 817 | - In ***lesson 192***, we change the **Security Rules** of our **Firestore Database** to allow read and write access to **authentified** users only. In the course, we add the following condition **`request.auth.uid != null`**. As of now (July 2022), we have to change this condition with the following one, regarding the [documentation](https://firebase.google.com/docs/rules/rules-and-auth#identify_users): **`request.auth != null`** 818 | ``` 819 | rules_version = '2'; 820 | service cloud.firestore { 821 | match /databases/{database}/documents { 822 | match /{document=**} { 823 | allow read, write: if 824 | request.auth != null // <-- here it is 825 | 826 | && request.time < timestamp.date(2022, 8, 5); // <-- if you wish to, you can add this condition that will only allow access to your database **before** a specified date 827 | } 828 | } 829 | } 830 | ``` 831 | :exclamation::exclamation::information_source: Keep in mind that the above rules are not secure enough at all. They are only good for developing purpose, but the moment you release your application in production (or even when you simply share it with other people that you don't know), you have to reinforce your **Security Rules** to be less permisive and have better and stronger control on whose accessing your database, and what data they have access to. 832 | 833 | -------------------------------------------------------------------------------- /images/guide/android_studio_sdk_tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DetainedDeveloper/App-Brewery-Flutter-Null-Safety/e7d84d33a6fcf29635d1ba75130fd6d72e099290/images/guide/android_studio_sdk_tools.png --------------------------------------------------------------------------------