One of the things I appreciate most about Dart, especially after having worked with languages that only support basic positional parameters, is how flexible its function parameter system is. When I’m designing a function signature in Dart, I have real choices — I can force certain parameters to always be passed in order, I can make others optional, and I can let callers pass parameters by name for clarity. Once I got comfortable with all three styles, my code became noticeably easier to read and far less error-prone, especially in Flutter widgets where constructors sometimes take a dozen parameters.
In this article, I’ll walk through positional parameters, optional positional parameters, named parameters, and return types — how each works, when I reach for one over another, and what’s actually happening under the hood with null safety and default values.
Positional Parameters: The Default Behavior
Positional parameters are the ones I write first when learning any language — they’re required, and they’re matched to arguments strictly by order.
int add(int a, int b) {
return a + b;
}
void main() {
print(add(5, 3));
}
Output:
8
Here, a and b are both required positional parameters. If I call add(5) without the second argument, Dart’s analyzer flags it immediately as a compile-time error, not something that quietly turns into a null value like it might in a more loosely typed language. This strictness is one of the things that makes me trust Dart’s type system — mistakes get caught before I even run the app.
Optional Positional Parameters
Sometimes I want a parameter to be optional but still passed positionally, without a name. I do this by wrapping the parameter in square brackets [].
String buildName(String first, [String? last]) {
if (last != null) {
return '$first $last';
}
return first;
}
void main() {
print(buildName('Ali'));
print(buildName('Ali', 'Khan'));
}
Output:
Ali
Ali Khan
Notice that last is typed as String? — nullable — because if I don’t provide a value, Dart needs somewhere to put the “absence” of a string, and that’s null. This is a direct consequence of sound null safety: optional parameters without a default value must be nullable types, or the analyzer won’t let me compile.
I can also give optional positional parameters a default value instead of leaving them nullable:
String buildGreeting(String name, [String greeting = 'Hello']) {
return '$greeting, $name!';
}
void main() {
print(buildGreeting('Sara'));
print(buildGreeting('Sara', 'Welcome'));
}
Output:
Hello, Sara!
Welcome, Sara!
When I provide a default value like this, the parameter no longer needs to be nullable, since it always has a concrete value even when the caller omits it.
Named Parameters
This is the feature I use the most, especially once I started writing Flutter widgets, because Flutter’s own constructors are built almost entirely around named parameters. I define them using curly braces {}.
void createUser({required String name, int age = 18, String? email}) {
print('Name: $name, Age: $age, Email: ${email ?? "Not provided"}');
}
void main() {
createUser(name: 'Bilal');
createUser(name: 'Sara', age: 25, email: 'sara@example.com');
}
Output:
Name: Bilal, Age: 18, Email: Not provided
Name: Sara, Age: 25, Email: sara@example.com
A few things stand out to me here:
requiredmarks a named parameter as mandatory, even though it’s inside curly braces. Withoutrequired, named parameters are optional by default.agehas a default value of18, so it doesn’t needrequiredor a nullable type.emailis nullable, since it has neither a default value nor therequiredkeyword.- Because the parameters are named, I can call
createUserand pass arguments in any order I want, which makes call sites far more self-documenting than a long list of positional values.
I genuinely believe named parameters are one of Dart’s best readability features. Compare:
Container(width: 100, height: 50, color: Colors.blue)
to what that would look like with purely positional parameters — I’d have no idea what 100 and 50 actually mean without checking the function signature. Named parameters remove that ambiguity entirely.
Mixing Parameter Types
Dart allows me to mix required positional parameters with either optional positional parameters or named parameters — but not both optional-positional and named in the same signature.
void logMessage(String tag, String message, {bool showTimestamp = false}) {
var prefix = showTimestamp ? '[${DateTime.now()}] ' : '';
print('$prefix[$tag] $message');
}
void main() {
logMessage('INFO', 'App started');
logMessage('ERROR', 'Something failed', showTimestamp: true);
}
Output:
[INFO] App started
[2026-07-29 10:00:00.000] [ERROR] Something failed
This pattern — required positional parameters up front, followed by named parameters for configuration-style options — is something I reach for constantly. It mirrors how Flutter itself is designed: a required child or data argument, followed by a long list of named styling and behavior options.
Return Types
Every Dart function has a return type, even if I don’t always write it explicitly. If I omit it, Dart infers dynamic, but I’ve made it a personal habit to always specify return types explicitly — it makes my intent clear and lets the analyzer catch mistakes early.
int square(int x) {
return x * x;
}
String describe(int x) {
return 'The square of $x is ${square(x)}';
}
void printResult(int x) {
print(describe(x));
}
void main() {
printResult(4);
}
Output:
The square of 4 is 16
void tells me — and anyone reading my code — that this function doesn’t return anything meaningful; it’s called purely for its side effects, like printing to the console.
Returning Nullable Types
With null safety, if a function might not always have a value to return, I need to be explicit about it by marking the return type as nullable.
int? findFirstEven(List<int> numbers) {
for (var number in numbers) {
if (number % 2 == 0) {
return number;
}
}
return null;
}
void main() {
print(findFirstEven([1, 3, 5, 6, 7]));
print(findFirstEven([1, 3, 5]));
}
Output:
6
null
Because I declared the return type as int?, the compiler forces anyone calling this function to handle the possibility of null before treating the result as a plain int. This has saved me from a lot of the classic “null reference” bugs I used to run into in other languages, since Dart won’t let me use a nullable value as if it were guaranteed non-null without an explicit check or the ! operator.
Returning Futures and Async Functions
Once I started working with network calls and databases, return types expanded to include Future<T> for asynchronous work.
Future<String> fetchUserName() async {
await Future.delayed(Duration(seconds: 1));
return 'Hamza';
}
void main() async {
print('Fetching...');
var name = await fetchUserName();
print('User: $name');
}
Output:
Fetching...
User: Hamza
The return type Future<String> tells me — and the compiler — that this function doesn’t return a String immediately; it returns a promise of a String at some point in the future. I await it to get the actual value once it resolves.
Real-World Example: Flutter Widget Constructor
This is where all of the parameter types come together in a way I use every single day. Here’s a simplified custom widget:
import 'package:flutter/material.dart';
class CustomButton extends StatelessWidget {
final String label;
final VoidCallback onPressed;
final Color backgroundColor;
final double borderRadius;
const CustomButton({
super.key,
required this.label,
required this.onPressed,
this.backgroundColor = Colors.blue,
this.borderRadius = 8.0,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius),
),
),
child: Text(label),
);
}
}
Here, label and onPressed are required named parameters, while backgroundColor and borderRadius are optional named parameters with sensible defaults. This is exactly the pattern Flutter itself uses across nearly every built-in widget, and once I adopted it in my own custom widgets, my code started to feel a lot more consistent with the rest of the framework.
Common Mistakes I’ve Made (and Seen Others Make)
- Forgetting
requiredon named parameters that should be mandatory. Without it, the parameter silently becomes optional, and if it’s not nullable and has no default, the analyzer will actually stop me — a good example of null safety catching a mistake before runtime. - Mixing optional positional and named parameters in the same function signature. Dart doesn’t allow this; I have to pick one style for the optional part of a function’s parameter list.
- Forgetting that default values must be compile-time constants. I can’t set a default value that depends on a runtime computation — it has to be something Dart can resolve statically, like a literal or a
constexpression. - Overusing positional parameters for functions with many arguments. Once a function has more than two or three parameters, I switch to named parameters to keep call sites readable.
Performance and Internal Behavior
Parameters in Dart are passed by value for primitives and by reference for objects, though it’s more accurate to say Dart passes references by value — meaning if I pass a mutable object like a List, the function receives a reference to the same underlying object, and mutations inside the function will be visible to the caller. This is important to keep in mind:
void addItem(List<int> list) {
list.add(99);
}
void main() {
var numbers = [1, 2, 3];
addItem(numbers);
print(numbers);
}
Output:
[1, 2, 3, 99]
I didn’t return anything from addItem, but numbers was still modified, because list inside the function pointed to the exact same list object in memory. This trips people up sometimes, especially if they’re expecting Dart to make a copy of the argument. If I want to avoid mutating the caller’s list, I need to explicitly create a copy first, using something like List.of(list) or the spread operator.
Best Practices I Follow
- I always specify explicit return types, even when Dart could infer them, for clarity and to catch mistakes early.
- I use named parameters for any function with more than two or three arguments, or wherever the meaning of an argument isn’t obvious from context.
- I mark parameters
requiredwhenever the function genuinely cannot function correctly without them. - I give sensible default values to optional parameters instead of leaving them nullable when a sensible default actually exists.
- I avoid deeply mutating objects passed as parameters unless that’s clearly the function’s intended purpose (like
addItemabove); otherwise, I return a new object instead.
Frequently Asked Questions
Can I combine required positional parameters, optional positional parameters, and named parameters all in one function? No. You can combine required positional with optional positional ([]), or required positional with named ({}), but not optional positional and named together in the same signature.
What happens if I don’t specify a return type? Dart infers dynamic, meaning the function’s return value can be treated as any type. I avoid this since it defeats a lot of the benefit of Dart’s static analysis.
Do named parameters have to be in a specific order at the call site? No — that’s part of their benefit. Named parameters can be passed in any order in the function call, since they’re matched by name, not position.
Is there a performance difference between positional and named parameters? No meaningful difference at runtime. The choice is purely about readability and API design, not performance.
Summary
Dart’s parameter system gives me a level of expressiveness I didn’t fully appreciate until I started building larger Flutter apps. Positional parameters are best for simple, order-obvious functions. Optional positional parameters let me add flexibility without demanding every caller provide every value. Named parameters — especially combined with required and default values — make my function signatures self-documenting and far less error-prone at the call site. Pairing all of this with explicit, sometimes nullable, return types means the compiler is doing a huge amount of work to catch my mistakes before the app even runs.
References
- Dart Language Tour — Functions: https://dart.dev/language/functions
- Dart Language Tour — Named Parameters: https://dart.dev/language/functions#named-parameters
- Effective Dart — Usage Guidelines: https://dart.dev/effective-dart/usage
- Flutter Documentation: https://docs.flutter.dev