-
DISCLAIMER : This repository is not an official outcome of London AppBrewery Team
-
This repository is made to help new students get through AppBrewery Flutter Course
-
This repository contains notes for only coding (project) sections and explains what has changed and what's the difference.
-
If something is not covered here, start a discussion, not an issue! I will try to add it then.
-
Use latest versions of required
packages
andplugins
, find them on pub.dev
1. Terminology
3. Resources
- You will often come across
deprecated
stuff, where it says This isdeprecated
. This means it's not recommended to use it anymore in your projects. You should avoid it and use alternatives.
-
Null safety is not your enemy! It's there you help you so you don't accidentally make something null and crash your app.
-
Dart has
sound null safety
. Basically, if you're writing any code that compiler thinks might end up beingnull
, it will notify you right away! Isn't that cool? -
Read more : Sound Null Safety, Understanding Null Safety, Null Safety in Flutter
-
Use latest
Flutter SDK
, currently I am using2.2
instable channel
- To upgrade old one, run
flutter upgrade
in yourTerminal / Command Prompt (cmd)
- To upgrade old one, run
-
There are couple of things that can cause this, I'll keep adding them in future! For now I have these solutions,
-
Just run
flutter doctor --android-licenses
-
Normally, this does the job. If it doesn't, go ahead.
-
Open Settings panel by,
-
File > Settings
(Windows and Linux) -
Android Studio > Preferences
(Mac)
-
-
Then navigate to,
Appearance & Behavior > System Settings > Android SDK
-
Select the
SDK Tools
Tab -
Select
Android SDK Command Line Tools
and clickApply
-
A dialog will pop up and ask you if you want to install these.
Click Yes/OK and let it install, after that, close
Android Studio
andrestart
it.
-
-
When I first encountered this issue, I thought there must be something wrong with just this particular update.
-
I searched it online, posted on Reddit, twitter, but found nothing.
-
Later on, I got to know that
New > Package
andNew > Directory (Folder)
options have now merged! -
So, to create a new
package
or just afolder
, simply useNew > Directory
option.
-
As we know Sound Null Safety was added to Flutter 2.
-
And before Flutter 2 old apps don't have Sound Null Safety feature.
-
So we need to fix this, and thank Flutter we have simple way to do.
-
To fix this issue easily ,
-
Delete all files and folders in App folder
except
course materialslib
-assets
-fonts
-pubspec.yaml
etc. -
For Example;
-
-
And then go to
Terminal
while its in project folder and-
Write this line to
Terminal
flutter create .
-
-
For Example;
- Then Flutter starts rebuilding application with migrated version of it. And Done!
1. Try out Null Safety on DartPad
2. Read Updated Flutter Docs
3. Watch and Follow Flutter's Official Youtube Channel
- To learn more about Null Safety and staying updated in general.
-
You right clicked on
res
folder but didn't findImage Asset
? Don't worry Follow these steps,-
Right click on
android
folder and a pop-up menu will open up.From that, select
Flutter > Open Android Module in Android Studio
If this doesn't work for you then follow these steps,
1. Close current project by pressing
File > Close Project
2. Now you will have the first screen of Android Studio.
3. Press
Open an Existing Project
, thenOpen File or Project
dialog will open.4. Here, navigate to your Flutter project in which, you want to add
Image Asset
5. Expand that and you will find
android
folder. Select that and pressOK
-
-
Both ways should open Android Part of your Flutter Project in
Android Studio
. -
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! -
After that long build process completes, you can find
Image Asset
option when you click onres
folder, Yay! -
Add assets and again,
File > Close Project
,Open an Existing Project
and this time, select your Flutter Project and continue!
FlatButton
isdeprecated
, so useTextButton
instead.
-
Getting a lengthy error when trying to use
audioplayers
plugin?-
All you need to do is open
android > build.gradle
(Project Levelgradle
file) -
Inside
buildscript {}
, you'll findext.kotlin_version
(Line 2 in file) -
Replace whatever version it is with Latest Stable Kotlin Version
-
As of July 23, 2021 it is,
ext.kotlin_version = '1.5.21'
-
Now, re-install the app. If it's already running, press Stop then press Run (Play) again.
-
-
FlatButton
isdeprecated
, so useTextButton
instead. -
By the end, the implementation of your
TextButton
should look like this:@override Widget build(BuildContext context) { return Expanded( child: TextButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all(color), ), onPressed: () { playSound(soundNumber); }, ), ); }
-
AudioCache
isdeprecated
, so useAudioPlayer
instead. -
By the end, your solution to playing the audio should look like this:
void playSound(int soundNumber) {
final player = AudioPlayer();
player.setSource(AssetSource('note$soundNumber.wav'));
}
- An example of a working project as of 16/07/2022 has been linked below:
-
Due to
null safety
, all variables in a class must have a value assigned, when created. If not, they must be declaredNullable
intentionally. This rule also applies toStateless
andStateful
widgets. On top of that, in classes extendingStatelessWidget
, all variables must be declaredfinal
-
So, make your
Question
class like this,class Question { String questionText; bool questionAnswer; Question(this.questionText, this.questionAnswer); // If you want named parameters // Question({required this.questionText, required this.questionAnswer}); }
-
@required
is replaced by justrequired
(Without @ sign) -
Here, the Keyword
this
points to current context, which happens to beQuestion
class.
-
-
FlatButton
isdeprecated
, so useTextButton
instead.
-
@required
is replaced by justrequired
(Without @ sign) -
So, while making
ReusableCard
, lesson shows you can skip usingcardChild
property, but that isn't possible, due tonull safety
-
This part is tricky, because now you can't have null arguments anymore.
-
So, you must have to intentionally make it
Nullable
, by adding?
to it, like this,class ReusableCard extends StatelessWidget { final Color colour; final Widget? cardChild; ReusableCard({required this.colour, this.cardChild}); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: colour, ), child: child, ); } }
-
Use it like
ReusableCard(color: Colors.amber)
and your app won't crash.
-
-
But, it's not same for
IconContent
,Icon
can havenull
value, butText
can't!class IconContent extends StatelessWidget { final IconData? icon; final String? label; IconContent({this.icon, this.label}); @override Widget build(BuildContext context) { return Column( children: [ Icon(icon), Text(label ?? ''), ], ); } }
-
So using
??
operator, you need to check if label isnull
or not, if it is, then you must provide aString
value to it. Here, I provided an empty String. -
Even if you don't pass any arguments like
IconContent()
, your app won't crash.
-
-
According to Lesson 129,
ReusableCard
now has a parameter namedonPress
, to get it working, use this,class ReusableCard extends StatelessWidget { final Color colour; final Widget? cardChild; final void Function()? onPress; ReusableCard({required this.colour, this.cardChild, this.onPress}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onPress, child: Container( decoration: BoxDecoration( color: colour, ), child: child, ), ); } }
- Because
GestureDetector
'sonTap
property wantsvoid Function()?
as argument.
- Because
-
In Lesson 128,
_InputPageState
has a new variable which haven't been initialized. As I already told you, you must initialize them or make themNullable
.class _InputPageState extends State<InputPage> { Gender? selectedGender; }
- Here, making it
Nullable
will do the job. Rest of the code will work perfectly fine.
- Here, making it
-
When running this app on a Physical Device running on:
-
Android
: you will need Internet Permission because the app sends arequest
to the OpenWeatherMapAPI
. For this, openAndroidManifest.xml
by navigating to,android > app > src > main > AndroidManifest.xml
and add the following line,
<uses-permission android:name="android.permission.INTERNET"/>
Keep the existing location permissions and add this above/below them. Add it under
manifest
tag, like this,<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="detaineddeveloper.example.clima"> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <!--Keep the existing location permissions above (whichever you have added previously)--> <uses-permission android:name="android.permission.INTERNET"/> <application android:label="clima" android:icon="@mipmap/ic_launcher"> . . . </application> </manifest>
-
iOS
:- There is no required Internet Permission.
- When launching the app, you will probably have a message asking you for Local Network Permission: it is not required either.
- 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):
- Open the Flutter project's Xcode target with open ios/Runner.xcworkspace
- Select the 'Runner' project in the navigator then the 'Runner' target in the project settings
- Make sure a 'Development Team' is selected under Signing & Capabilities > Team. You may need to:
- Log in with your Apple ID in Xcode first
- Ensure you have a valid unique Bundle ID (for example "com.put-your-name-here.clima")
- Register your device with your Apple Developer Account
- Let Xcode automatically provision a profile for your app
- Select your iOS physical device as the target and click the Run button
- 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)
- ❗ 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
-
-
If your app cannot retrieve your current location on your
iOS
physical device it is probably because the geolocator 8.2.1 flutter package has been updated and you will need to apply the following changes:-
As of now (June 2022), the geolocator package indicates to add both
NSLocationWhenInUseUsageDescription
andNSLocationAlwaysUsageDescription
permissions to access Location Service. Since iOS 11, theNSLocationAlwaysUsageDescription
property key is deprecated. Use instead only one of those permissions:- the
NSLocationAlwaysAndWhenInUseUsageDescription
to enable the Location Service in foreground and background, - the
NSLocationWhenInUseUsageDescription
to enable the service in foreground only, as recommended by Apple.
To do so, open
Info.plist
file by navigating to:ios > Runner > Info.plist
and add the following line right under the
<dict>
tag (your can customise the message in the<string>
tag, it has to explain why your app needs to have access to that particular permission):<dict> <key>NSLocationWhenInUseUsageDescription</key> <string>This app needs access to your location to provide weather data of your current location.</string> </dict>
- the
-
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 thelocation.dart
file:- Short version:
Future<void> getCurrentLocation() async { try { LocationPermission locationPermission = await Geolocator.requestPermission(); if (LocationPermission.whileInUse == locationPermission || LocationPermission.always == locationPermission) { Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.lowest); latitude = position.latitude; longitude = postion.longitude; } } catch (e) { print(e); } }
- Long but more complete version:
Future<void> getCurrentLocationCheckingPermissions() async { bool serviceEnabled; LocationPermission locationPermission; // Test if location services are enabled. serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { // Location services are not enabled don't continue // accessing the position and request users of the // App to enable the location services. return Future.error( 'Location services are disabled. Please activate them.'); } else { locationPermission = await Geolocator.checkPermission(); if (LocationPermission.unableToDetermine == locationPermission) { return Future.error( 'Unable to determine if location permissions are enabled.'); } else if (LocationPermission.denied == locationPermission || LocationPermission.deniedForever == locationPermission) { print(Future.error('Location permissions are denied: ' + locationPermission.toString())); locationPermission = await Geolocator.requestPermission(); } if (LocationPermission.whileInUse == locationPermission || LocationPermission.always == locationPermission) { Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.lowest); this.latitude = position.latitude; this.longitude = position.longitude; } } }
- Short version:
-
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.
- 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 aniOS application
if you plan to deploy on iOS. - 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 🎊 , so choose that option instead, and follow the instructions that will be displayed:
- Depending on your Operating System (Windows, macOS or Linux), you will find the steps to do on the documentation.
- After the installation is done, connect to it by executin the following command in a terminal:
firebase login
.
- If you have arrived this far in the course, you have already installed the Flutter SDK a long time ago!
- The Flutter project has alo been created already, it's our Flash Chat Flutter project.
- 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)
-
Before executing the FlutterFire CLI, make sure to change your
Application ID
for Android, and youriOS 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):-
Application ID
for Android:- Open the
build.gradle
file located underandroid > app > build.gradle
- 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)
- Open the
-
iOS Bundle ID
for iOS:- Right-click on the "ios" folder and choose
Flutter > Open iOS module in Xcode
- 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
- You should find a Bundle Identifier like co.appbrewery.flashChat --> change it for something unique and personal like com.firstnamelastname.flashChat
- Right-click on the "ios" folder and choose
-
-
After changing the
Application ID
for Android, and/or theiOS 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:flutterfire configure --project=YOUR_FIREBASE_PROJECT_ID_HERE
(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
) -
After running the previous command, you should find in your Flutter project:
-
Flutter
: your Firebase configuration file underlib > firebase_options.dart
(the most important one, it contains both your Android and your iOS API keys to access Firebase services) -
Android
: your Firebase configuration file underandroid > app > google-services.json
-
iOS
: a Firebase identifying file underios > firebase_app_id_file.json
(if the course is not updated, you might see that you should have a file calledGoogleService-Info.plist
instead underios > Runner > GoogleService-Info.plist
--> I am not an expert with Firebase, but my guess is that theGoogleService-Info.plist
comes up when you configure your Firebase project by adding an iOS application instead of a Flutter application)
-
-
To initialise Firebase, start by adding to your Flutter project the
firebase_core
plugin:flutter pub add firebase_core
-
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:
flutterfire configure
-
Then, if everything's fine, in your
lib/main.dart
file, change the main method to use the Firebase initialising methodFirebase.initializeApp()
import 'package:firebase_core/firebase_core.dart'; import 'package:flash_chat/firebase_options.dart'; Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); runApp(FlashChat()); }
❗❗ 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 ofDefaultFirebaseOptions.currentPlatform
, you will notice that it corresponds to yourlib > firebase_options.dart
, and that it simply verifies the platorm on which your application is running, then returns the appropriate configuration. -
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).
If you encounter some errors, please have a look below to find some fixes that may be of help:
-
On Android
:- The
firebase_core
plugin (as well as all Firebase plugins) requires at least theAndroid SDK version 31
now (July 2022), so make the modification if needed underandroid > app > build.gradle
:
android { compileSdkVersion 31 ... }
- The
-
On iOS
:- There was no problem encountered on my side. Please add more details about yours in the Discussions section if you have troubles here.
-
-
Add the
firebase_auth
and thecloud_firestore
plugins in your Flutter project:flutter pub add firebase_auth flutter pub add cloud_firestore
Click on
Pub Get
to make sure that you get the dependencies in your project. -
On Android
:-
You may need to upgrade the
minSdkVersion
of the Android part of your Flutter project, underandroid > app > build.gradle
, because some Firebase plugins have a minimal requirement of the SDK version 21 now (July 2022), like thecloud_firestore
plugin for instance, so apply that change:android { ... defaultConfig { ... minSdkVersion 21 ... } ... }
-
Those plugins require as well the
Android SDK version 31
, so make sure that you have it updated in yourandroid > app > build.gradle
:android { compileSdkVersion 31 ... }
-
After adding the dependencies in the AndroidManifest.xml file and the build.gradle files, you might get this error:
ERROR:D8: Cannot fit requested classes in a single dex file (# methods: 104246 > 65536) com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: The number of method references in a .dex file cannot exceed 64K. ... * What went wrong: Execution failed for task ':app:mergeExtDexDebug'. > A failure occurred while executing com.android.build.gradle.internal.tasks.DexMergingTaskDelegate > There was a failure while executing work items > A failure occurred while executing com.android.build.gradle.internal.tasks.DexMergingWorkAction > com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: The number of method references in a .dex file cannot exceed 64K. Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html
To fix this issue, inside the app-level build.gradle file,
../android/app/build.gradle
, the minSdkVersion property under the defaultConfig block in the android block should be set to21
to avoid error. This is because multiDex support is enabled by default for sdkVersion 21. -
Additional dependencies have to be added to the app-level build.gradle file,
../android/app/build.gradle
:dependencies { ... implementation platform('com.google.firebase:firebase-bom:30.0.1') implementation 'com.google.firebase:firebase-auth' implementation 'com.google.firebase:firebase-firestore' ... }
-
To enable internet connectivity on a physical device, add
<uses-permission android:name="android.permission.INTERNET" />
to the AndroidManifest.xml file.
-
-
On iOS
:- Before running the application to check if everything is fine, update
CocoaPods
:pod repo update sudo gem install cocoapods pod setup
- Run your app to check if it's working:
- If you encounter the following error:
update your
Error output from Cocoapods: [!] 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 Error running pod install Error launching application on iPhone.
Podfile
file underios > 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: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). 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.# Uncomment this line to define a global platform for your project platform :ios, '10.0'
- If you encounter the following error:
- Before running the application to check if everything is fine, update
-
In chat_screen.dart file, the object type for the loggedInUser was previously
FirebaseUser
, and should now be replaced withUser
. Moreover, the Firestore instance is now retrieved withFirebaseFirestore.instance
instead ofFirestore.instance
:class _ChatScreenState extends State<ChatScreen> { final _firestore = FirebaseFirestore.instance; final _auth = FirebaseAuth.instance; User loggedInUser; String message; ... }
-
When retrieving the messages from your Firestore Database, you will notice some changes in the API:
-
The
getDocuments
method has been renamed toget
:_firestore.collection('messages').getDocuments(); // <-- BEFORE _firestore.collection('messages').get(); // <-- NOW
-
The QuerySnapshot property, documents, has been renamed to docs.
void getMessages() async { final messages = await _firestore.collection('messages').get(); for (var message in messages.documents) { // <-- BEFORE print(message.data()); } for (var message in messages.docs) { // <-- NOW print(message.data()); } }
-
-
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 theAsyncSnapshot
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()
):StreamBuilder<QuerySnapshot<Map<String, dynamic>>>( // <-- By adding the type <QuerySnapshot<Map<String, dynamic>>... stream: _firestore.collection('messages').snapshots(), builder: (BuildContext context, AsyncSnapshot snapshot) { final messages = snapshot.data.docs; // <-- ... you will be able to access the 'docs' property because we manipulate a Stream of QuerySnapshot for (var message in messages) { 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<String, dynamic> final sender = message.data()['sender']; ... } ... }, );
-
To deal with the case where we would have no data in our
StreamBuilder
, we do add aCircularProgressIndicator
after checking the value of!snapshot.hasData
. To center it on the screen, add a margin usingMediaQuery
to retrieve the height of the screen (if you want to see how it looks like, you can simply temporarily change the if condition withtrue
):StreamBuilder<QuerySnapshot<Map<String, dynamic>>>( stream: _firestore.collection('messages').snapshots(), builder: (context, snapshot) { if (!snapshot.hasData) { // <-- change it temporarily to 'if (true)' to quickly see the result return Container( margin: EdgeInsets.only(top: MediaQuery.of(context).size.height / 3), child: CircularProgressIndicator( backgroundColor: Colors.lightBlueAccent, ), ); } ... }, );
❗ℹ️ To avoid your
CircularProgressIndicator
to be stretched horizontally and look weird, make sure that the column in which yourMessagesStream
is into doesn't have the propertycrossAxisAlignment: CrossAxisAlignment.stretch
:body: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, // crossAxisAlignment: CrossAxisAlignment.stretch, // <-- remove this if you have it children: <Widget>[ MessagesStream(), Container(...), ...
-
The sorting of the messages in the chat screen is still chaotic even after the reversal in lesson 191. To fix this problem, we need to add a timestamp field to the messages and sort the collection based on it as shown here:
... TextButton( onPressed: () { controller.clear(); _firestore.collection('messages').add({ 'text': messageText, 'sender': loggedInUser.email, 'timestamp': FieldValue.serverTimestamp(), // Here is the **timestamp** field. }); }, ...
We use the server time instead of generating a timestamp with the user device because:
- Our users may be in different timezones so the time differences will affect our app.
- Some devices could be set to incorrect times.
class MessagesStream extends StatelessWidget { @override Widget build(BuildContext context) { return StreamBuilder<QuerySnapshot>( stream: _firestore.collection('messages').orderBy('timestamp').snapshots(), // Here, the **.orderBy** sorts the messages according to the server timestamps. builder: (context, snapshot) { ...
- 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:request.auth != null
❗❗ℹ️ 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.rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.auth != null // <-- here it is && 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 } } }