Flutter is one of the fast-growing technology when it comes to cross-platform development, and the secret sauce behind making a Flutter application is the Dart language.
While you can start developing a Flutter app even if you are unfamiliar with Dart, this tutorial will cover the essential syntax and information you need to know in order to feel comfortable building a Flutter app.
Dart is a versatile and client-optimized language for fast-developing apps on any web or mobile platform, and can be used on desktop and embedded devices. The core goal of Dart language is to provide you with a set of tools and a programming language that makes you productive and evolves as developers’ requirements and needs grow.
Dart is a comprehensive language and offers excellent language capabilities, such as Future, Stream, Sound Null Safety, etc.
Dart’s designed to be familiar to most developers with various backgrounds in programming. So, no matter if have a background in JavaScript and TypeScript, or if you have been an object-oriented programmer, you’ll find working with Dart familiar.
And, because of Dart’s architecture, killer feature, hot reload, and declarative programming are all possible in Flutter.
More importantly, Dart also comes with many built-in libraries, such as dart:async, dart:convert, dart:html, dart:io, etc., as well as a fantastic ecosystem and outstanding package manager pub.dev.
Whether you want to use Flutter or not, Dart would be a great choice to learn and use in your next application.
If you’d like to give it a try quickly, you can use dartpad.dev online.
Before you start with creating a Flutter application, you should know a few Dart concepts:
The entry point of every app is the main()
function. Even if you want to print something in the console, you must have a main()
part.
void main() { var list = ['apples', 'bananas', 'oranges']; list.forEach((item) { print('${list.indexOf(item)}: $item'); }); }
In Flutter, you will start your application from the main()
function in the PROJECT_ROOT/lib/main.dart
, where you pass your main widget to runApp()
that will attach it to the screen. That’s the first main entry point.
void main() { runApp(MyApp()); }
:
(semicolon):You need ;
(semicolon) in Dart, as you can see in the example above:
runApp(MyApp());
Dart is a type-safe language. It uses static type checking and runtime checks. When you learn the syntax, you understand Flutter code quicker. Here is the anatomy of a simple variable:
[MODIFIER] [TYPE] [VARIABLE_NAME] = [VALUE];
// e.g: final String name = "Majid Hajian";
Although types are mandatory, type annotations are optional because of type inference. So, you may encounter this:
var name = "Majid Hajian"; // from now on `name` is a String;
The most common initializing variables modifiers in Dart are var
, final
, const
, and late
, but keep in mind that you can use all modifiers except var
when you use type before the variable name.
var name = "Majid Hajian"; String name = "Majid Hajian"; final String name = "Majid Hajian"; const String name = "Majid Hajian";
Using var
or using no modifier creates a flexible variable, which means you can change the value anytime. If you never intend to modify the variable, you should use final
, which sets the variable only once, or const
, which forms a compile-time constant.
But there are more complex situations. Let’s take a look at Map
and List
type definition:
// Type of a List (Array): List<TYPE_OF_MEMBER> // e.g: List<String> names = ['Majid', 'Hajian']; // Type of a Map (Key-Values): Map<Key_TYPE, VALUE_TYPE> // e.g: Map<String, number> ages = {'sara': 35, 'susan: 20};
In many cases, you may not provide enough information to the Dart analyzer, and you may face a type casting error. Let’s see an example:
var names = [];
The variable types infer List<dynamic>
and dynamic
could be any type because we do not provide the array’s possible type when we initialize the variable. Therefore, Dart cast the type to List<dynamic>
where it could be anything. By adding an annotation to the value while initializing or launching the variable, we can prevent this type of error.
final names = <String>[]; // Or final List<String> names = [];
As of Dart 2.12, Dart is a sound null safe language, which means types in your code are non-nullable by default, and that indicates a variable cannot contain null
unless you say they can.
final String name = null; // or final String name;
Notice that the variable above is not valid anymore because we initialize a variable with a null
value. Because we haven’t specified that yet, runtime null-dereference errors turn into edit-time analysis errors.
Here is when ?
comes handy. To assign the variable with the null
value, we can use ?
to its type declaration.
final String? name = null;
You’ll see this syntax often in Flutter 2.0.0+ together with Dart 2.12.0+.
Finally, the most common built-in types in Dart that you will find on a Flutter application are the following:
Dart is an object-oriented language with classes and mix-in base inheritance. That means you can create abstract
types, class
, use implement
, and extends
. You may also see with
where you want to use a mix-in.
In Dart classes, the constructor
name is the same as className
, like so:
class MyApp { MyApp(); // constructor }
You don’t need to have a constructor if you do not need to initialize the instance variable or create an object. In case you need that, you should pass them via constructor parameters.
class MyApp { MyApp(this.title); final String? title; }
In Dart 2, you don’t need to use new
keyword to instantiate a class, either.
final myapp = MyApp('Majid Hajian'); print(myapp.title);
All widgets in Flutter are an extended class of StatelessWidget
or StatefulWidget
. Hence, you can create your widget (class):
class MyApp extends StatelessWidget { }
StatelessWidget
and State
object corresponded to StatefulWidget
have both build()
method to build your screens. So, to implement your widgets to be built, you must @override the build() method.
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return Container(); } } class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { @override Widget build(BuildContext context) { return Container(); } }
The convention is to start className
with a capital letter.
It’s imperative to learn how you can define parameters, whether in a class or a function, as it’s one of the crucial parts of Flutter development.
In Dart, if you want to define a required parameter, you can pass them to a constructor or the function.
String getUrl(String prefix, String host) { return ''; } // OR class MyApp { MyApp(this.title); final String? title; }
In both cases, you need to pass the parameters correctly to the expected position. That’s what we name positional parameters, too.
In many situations, you’ll find that you’ll want to make a parameter optional. For example, to change the code above, we can code like this:
String getUrl({String? prefix, String? host}) { return ''; } // OR class MyApp { MyApp({this.title}); final String? title; }
We use {}
to define our optional parameters that are named as we described. Now, to use the named parameters, use the name of the parameters and assign the value.
getUrl( host: '', prefix: ''); //Or MyApp(title: 'Majid');
As you can see, the main advantage of using this approach is that you do not need to pass the value to the parameter’s exact position.
More importantly, Your functions and class parameters are self-documented. In other words, you can simply define what the name of param is and pass the value. Defining name parameters is helpful when you want to specify many parameters for one method or class.
You’ll come across named parameters in Flutter often. Here is an example of the Text
widget in Flutter:
Text( this.data, { Key key, this.style, this.strutStyle, this.textAlign, this.textDirection, this.locale, this.softWrap, this.overflow, this.textScaleFactor, this.maxLines, this.semanticsLabel, this.textWidthBasis, this.textHeightBehavior, })
The this.data
is positional, meaning the first argument is mandatory to pass in, but the rest of the parameters are optional as they are defined within {}
.
You may not ask how a named parameter can be required instead of optional. In Dart 2.12+, you now have the required
keyword that makes an argument to become mandatory to pass in. Let’s look at the example above.
class MyApp { MyApp({this.title}); // optional named parameter final String? title; }
But if you use the required
keyword before the argument, we will make it mandatory to pass it in, even though it’s a named parameter.
class MyApp { MyApp({required this.title}); final String? title; }
If you now instantiate MyApp()
class, you have to pass title
too; otherwise, the compiler will throw an error.
print(MyApp(title: 'Majid'));
Dart comes with a formatting tool that helps make your code more readable. Here’s a tip that will help you format your code much better, especially in Flutter, where you will have many nested widgets. Use ,
where you can!
Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('You have pushed the button this many times:'), Text('$_counter', style: Theme.of(context).textTheme.headline4), ], ),
Here is a Column
widget that has two Text
children. None of the children use ,
where they pass arguments. The text is cramped and not easily readable, but if you use ,
at the end of the last parameter in each Text
widget, it will be formatted and more friendly.
Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), ], ),
You’ll get the formatting out-of-the-box for free with the format tool in the command line or your editor of choice together with the Dart plugin.
You may define a function inside a class — i.e., methods — or in top-level. Creating a function as simple as the syntax below.
// top-level getMyname() { // logic } // OR class MyClass() { getMyName() { } }
Dart provides asynchronous programming via Future or Stream. To define a Future
, you can use the async
keyword.
Future<String> getUrl({String? prefix, String? host}) async { return 'd'; }
And to wait until the value is resolved, you can use the await
keyword.
main() async { final url = await getUrl(prefix: '', host: ''); }
You should use the await
keyword wrapped in a function/method with the async
keyword.
To create a Stream
, you’ll use async*
keyword. Now, you can subscribe to the stream and get the value every time that it is emitted until you cancel this subscription.
getUrl(prefix: '', host: '').listen( (String value) { print(value); }, );
Notice that the listen()
function accepts a function, i.e., a callback, and because everything is an object in Dart, you can pass them around even in functions. This is commonly used in Flutter when events are occuring, such as onPressed
.
TextButton( onPressed: () { // pressed // logic }, child: Text('Submit'), )
Now, you should be able to read, understand, and write Flutter code. To prove it, let’s take an example:
class MyCustomWidget extends StatelessWidget { MyCustomWidget({this.counter}); final String? counter; @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Text( '$counter', style: Theme.of(context).textTheme.headline4, ), ], ), ); } }
First, you’ll create your custom widget where it uses extends
. Then, you’ll @override
the build()
method. You’ll return Center
, a Flutter pre-defined widget with several name parameters, including the child
where you assign Column
.
Column
has several name parameters where you use only mainAxisAlignment
and children
. You’ll have two Text
widgets where they have both positional and named parameters, and so on.
You’ll see now how easily you can understand this code, and you can now even write yours!
Flutter is a fantastic piece of technology that helps you create a cross-platform application, and Dart is its foundation. Dart is easy to learn when you know where to start and what to learn first.
In this article, we reviewed the most widely-used fundamentals in Flutter so that you can open a Flutter application and not only understand the initial syntax, but can write your Dart code too.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.