Flutter/Dart: Recursive & Callback Functions

and how to show dialogs regardless of the current page!

Soo Kim
4 min readApr 8, 2023

This post consists of very concise example of recursive and callback functions so do keep in mind that the code is simplified for this post.

Full Code for anyone that wants to skip the explanation.

Recursive Function Example

I had never had the opportunity to use a recursive function until recently, so I wanted to share how and when I used it. The code that executes the below scenario is the recursive function.

Scenario: user goes to a new page to write a review → submits a review → page is popped → SnackBar appears with an option to modify → if clicked on modify, review page with data is shown.

The scenario gets repeated when the user clicks “modify”, hence becoming a recursive function.

Recursive Function

class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ColoredTextButton(
text: "Write a Review",
onPressed: () async {

Future<void> _recursive({String review = ""}) async {
final String _review = await Navigator.of(context).push<String>(MaterialPageRoute(
builder: (_) => WriteReviewPage(review: review)
)) ?? "";

if (_review.isEmpty) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: const Text("Your review has been submitted.",
style: TextStyle(fontSize: 16.0 ),), duration: const Duration(seconds: 5),
action: SnackBarAction(
label: "Modify",
onPressed: () async => await _recursive(review: _review),
),
),
);
}

await _recursive();
},
),
),
);
}
}

Review Page

import 'package:flutter/material.dart';
import 'package:recursive_callback/color_button.dart';

class WriteReviewPage extends StatefulWidget {
const WriteReviewPage({Key? key, required this.review}) : super(key: key);
final String review;

@override
State<WriteReviewPage> createState() => _WriteReviewPageState();
}

class _WriteReviewPageState extends State<WriteReviewPage> {
final TextEditingController _controller = TextEditingController();

@override
void initState() {
super.initState();
if (this.widget.review.isNotEmpty) this._controller.text = this.widget.review;
}

@override
void dispose() {
this._controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: FocusManager.instance.primaryFocus?.unfocus,
child: Scaffold(
appBar: AppBar(title: const Text("Write A Review!"),),
body: SizedBox(
width: MediaQuery.of(context).size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 300.0,
height: 50.0,
margin: const EdgeInsets.only(bottom: 20.0),
child: TextField(
controller: this._controller,
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
constraints: BoxConstraints(),
border: OutlineInputBorder()
),
),
),

ColoredTextButton(
text: "Submit",
onPressed: () async {
/// send data to server
Navigator.of(context).pop(this._controller.text.trim());
}
),
],
),
),
),
);
}
}

Callback Functions

When I first started coding, it took me a few attempts to get used to callback functions. Basically, callback functions are functions passed as an argument into another function to (1) control the timing/order of functions and (2) use a value from another function.

One place I use callback functions the most is after receiving response from the server, passing the error or success messages as an argument. In this example, if it’s an error message, I show a pop up dialog, and if its a success message, I show a Snackbar.

API Service

import 'dart:async' show FutureOr;
import 'dart:convert' show json, utf8;
import 'dart:io' show SocketException;
import 'package:http/http.dart' as http;

typedef Json = Map<String, dynamic>;

class APIService {
final String _baseUrl = "";

String _path(String path){
String _path = path.trim();
if (!_path.startsWith("/")) _path = "/" + _path;
return _path;
}

Map<String, String> _headers({required String? accessToken}) => {"content-type":"application/json", "authorization": "Bearer $accessToken"};

Future<void> postToServer({
required String path,
Map<String, String>? headers,
required String? accessToken,
required dynamic body,
FutureOr<void> Function(String)? errorCb,
FutureOr<void> Function(String)? successCb,
}) async {
try {
final http.Response _res = await http.post(
Uri.parse(this._baseUrl + this._path(path)),
headers: {...headers ?? {}, ...this._headers(accessToken: accessToken)},
body: json.encode(body),
).timeout(const Duration(seconds: 13), onTimeout: () => http.Response("null", 404));

final Json _decodedBody = json.decode(utf8.decode(_res.bodyBytes)) as Json;
if (_decodedBody.containsKey("error") && errorCb != null) await errorCb(_decodedBody["error"]);
if (_decodedBody.containsKey("success") && successCb != null) await successCb(_decodedBody["success"]);
} catch (e) {
print(e);
if (e.runtimeType == SocketException && errorCb != null) await errorCb("Cannot connect to the server.");
}
}

This way, the user can continue using the app without having to wait for the future to complete. When the future does complete, the user can be notified.

Review Page with Callback Function

Do note, the page will most likely be popped before the error or success callback function is executed. This means that the “context” your usually pass on to dialog or ScaffoldMessenger — which belongs to the current widget (in this case, Write Review Page State) — will not be available.

In order for you to show the dialog wherever the user is, you’d want to use the context of the app itself, which you can do by assigning a navigator key to the MaterialApp. Then, you can use this key to find the context of the app.

final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

class Main extends StatelessWidget {
const Main({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
navigatorKey: navigatorKey,
);
}
}
ColoredTextButton(
text: "Submit",
onPressed: () async {
final BuildContext? _appContext = navigatorKey.currentContext;
if (_appContext == null) return;

await _service.postToServer(
path: "/review",
accessToken: "",
body: {"review": this._controller.text.trim()},
errorCb: (String errorMsg) async {
await showDialog(
/// if you use context of this WriteReviewPage, because the page already got disposed, an error occurs
context: _appContext,
builder: (_) => Dialog(child: Text(errorMsg))
);
},
successCb: (String successMsg) async {
ScaffoldMessenger.of(_appContext).showSnackBar(
SnackBar(content: Text(successMsg,
style: const TextStyle(fontSize: 16.0 ),), duration: const Duration(seconds: 1),
),
);
}
);
Navigator.of(context).pop();
}
)

Happy Coding!

--

--