ChatGPT Tutorial for Flutter: Getting Started
Learn how to incorporate ChatGPT into your Flutter apps! In this tutorial, see how to leverage machine learning and ChatGPT with a real-world trivia app. By Alejandro Ulate Fallas.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
ChatGPT Tutorial for Flutter: Getting Started
30 mins
Adding Widgets
Open lib/pages/trivia_game_page.dart and start by replacing // TODO: Fetch & display questions.
with the following code:
@override
void initState() {
// 1.
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
fetchQuestion();
});
super.initState();
}
// 2.
void fetchQuestion() {
setState(() {
fetchQuestionStatus = OperationStatus.loading;
hint = null;
fetchHintStatus = OperationStatus.idle;
});
Data.instance.generateTriviaQuestion().then(
(question) => setState(() {
currentQuestion = question;
fetchQuestionStatus = OperationStatus.success;
}),
);
}
// 3.
void resetGame() {
setState(() {
points = 0;
availableHints = 3;
isGameOver = false;
fetchQuestionStatus = OperationStatus.loading;
currentQuestion = null;
hint = null;
fetchHintStatus = OperationStatus.idle;
});
fetchQuestion();
}
// 4.
Widget buildGameView() {
if (isGameOver) {
return GameOver(
score: points,
correctAnswer: currentQuestion!.correctAnswer.key,
funFact: currentQuestion!.funFact,
onTryAgainPressed: () {
resetGame();
},
onGoBackPressed: () {
Navigator.pop(context);
},
);
}
if (fetchQuestionStatus.isLoading) {
return const Center(
child: LoadingView(),
);
}
// TODO: Add guard if for errors.
return Stack(
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.topLeft,
child: HUD(
playerName: widget.player,
score: points,
availableHints: availableHints,
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
QuestionDetail(
question: currentQuestion!,
onAnswerSelected: (question, answerKey) {
if (question.answers[answerKey] ?? false) {
setState(() {
++points;
});
fetchQuestion();
} else {
setState(() {
isGameOver = true;
});
}
},
),
// TODO: Call `buildHintView`
],
),
],
);
}
Remember to add the corresponding imports at the top of the file.
import '../data.dart';
import '../domain.dart';
import '../widgets/game_over.dart';
import '../widgets/hud.dart';
import '../widgets/loading_view.dart';
import '../widgets/question_detail.dart';
Time to explain the code step by step:
- You added a call for a post-frame callback to fetch a new question. This will allow the user to see a loading state UI and then the question generated.
- All
fetchQuestion
does is set a loading state while calling the data layer to generate a trivia question. If it’s successful, it updates the state with the information of the question. Otherwise, an error state is set. - You’ve also defined
resetGame
. This function resets the state to where it was at the beginning. It allows the player to start a new game after losing. -
buildGameView
is a helper function that will build all the UI needed for displaying theQuestion
. It usesif
statements to exit early when the game ends or if the question is loading.
Finally, update build
to call buildGameView
like this:
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 16.0,
horizontal: 24,
),
child: buildGameView(),
),
),
);
}
Build and run your project. You should be able to play and answer a couple of questions.
Great job, you now have a functional Wizz app.
But there’s a slight gotcha in the code.
Dealing With Unexpected API Responses
If you didn’t add billing information while creating your OpenAI account, the API will limit your calls to three per minute. While running the app, if you answer questions in quick succession, you’ll see the exception like this:
This is because OpenAI needs to protect its platform against bad actors and malicious attacks. You could do two things to mitigate this: catch errors from the API and/or add billing information to your account.
In any case, you should always try to handle errors in your code. That way, you’ll avoid app crashes or unexpected behaviors, plus you also get to update the screen’s information. Change fetchQuestion
to the following:
void fetchQuestion() {
setState(() {
fetchQuestionStatus = OperationStatus.loading;
hint = null;
fetchHintStatus = OperationStatus.idle;
});
Data.instance
.generateTriviaQuestion()
.then(
(question) => setState(() {
currentQuestion = question;
fetchQuestionStatus = OperationStatus.success;
}),
)
.onError(
(error, stackTrace) => setState(() {
fetchQuestionStatus = OperationStatus.failed;
}),
);
}
Next, replace // TODO: Add guard if for errors.
in buildGameView
with the code below:
if (fetchQuestionStatus.isFailed) {
return Center(
child: ErrorView(
onRetryPressed: () => fetchQuestion(),
),
);
}
This requires an import of error_view.dart
at the top of the file:
import '../widgets/error_view.dart';
With those changes, you’ve added an onError
callback to your API call and updated the state for the widget. Then, in buildGameView
, you added a new if
statement to exit early if there was a problem fetching a question.
Build and run the app. Try to make the exception pop up again. This time you should see the error view when that happens.
Adding Hints For Questions
One final feature Wizz should have is hints for the players when the questions get too hard.
Open data.dart. Replace // TODO: Request a hint for a trivia question
with the following:
Future<Hint> requestHint(Question question) async {
final prompt = requestHintPrompt(question.description);
_messages.add(OpenAIChatCompletionChoiceMessageModel(
content: prompt,
role: OpenAIChatMessageRole.user,
));
final response = await _generateChatCompletion();
final rawMessage = response.choices.first.message;
_messages.add(rawMessage);
return Hint.fromOpenAIMessage(rawMessage);
}
Next, open domain.dart. Add a constructor to parse OpenAI’s message into a Hint
, and remember to remove the TODO item. It should look like this:
factory Hint.fromOpenAIMessage(
OpenAIChatCompletionChoiceMessageModel message) {
final response = jsonDecode(message.content) as Map;
return Hint(
content: response['hint'] as String,
);
}
With both of these changes, you’ve prepared the data and domain layers to ask for hints and to send them back to the UI. Now it’s time to add some code so the UI can display them.
Open trivia_game_page.dart again. Replace // TODO: Fetch & display hints.
with the following code:
// A.
void fetchHint() {
if (currentQuestion == null || availableHints == 0) return;
setState(() {
fetchHintStatus = OperationStatus.loading;
});
Data.instance
.requestHint(currentQuestion!)
.then(
(newHint) => setState(() {
hint = newHint;
availableHints -= 1;
fetchHintStatus = OperationStatus.success;
}),
)
.onError(
(error, stackTrace) => setState(() {
fetchHintStatus = OperationStatus.failed;
}),
);
}
// B.
Widget buildHintView() {
if (availableHints == 0 && hint == null) return const SizedBox();
if (fetchHintStatus.isLoading) {
return const Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 24),
CircularProgressIndicator(),
],
);
}
if (hint == null || fetchHintStatus.isFailed) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 24),
TextButton(
onPressed: availableHints > 0 ? fetchHint : null,
child: Text(
'Not sure? Give me a hint! 🙏🏼',
style: TextStyle(
decoration: TextDecoration.underline,
decorationColor: Theme.of(context).colorScheme.primary,
),
),
)
],
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 24),
Text(
hint!.content,
textAlign: TextAlign.center,
),
],
);
}
Here’s what you just did:
-
fetchHint
follows pretty much the same pattern asfetchQuestion
. It updates the state to show a loading view and updates when the task completes. It also considers possible errors and exits early if no more hints are available. -
buildHintView
also follows the same pattern asbuildGameView
withif
statements to exit early when needed.
Finally, replace // TODO: Call buildHintView
in the trivia_game_page.dart file with an actual call to buildHintView
like so:
buildHintView(),
Build and run Wizz. You should see the following on the game page:
If you click it, you should then see it load a hint and display it afterward.
Here’s how it looks when the hint is loaded:
Great job! You’ve successfully added GPT features to Wizz. The owners must be so proud since they now get to use AI in their product, and it’s all because of you.