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.

5 (2) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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:

  1. 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.
  2. 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.
  3. 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.
  4. buildGameView is a helper function that will build all the UI needed for displaying the Question. It uses if 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.

Note: In case you see the error You exceeded your current quota, please check your plan and billing details., you have exceed the default 5$ credit on your account, or OpenAI changed their policies. You have to add your billing details to use OpenAI API. Testing this app costs less then 1$. Pricing is here

Wizz app: loading a question

Wizz app: game screen with trivia question

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:

Wizz app: rate limit exception for OpenAI's API

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.

Wizz app: error screen when rate limit exception 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 as fetchQuestion. 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 as buildGameView with if 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:

Wizz app: hint button displaying

If you click it, you should then see it load a hint and display it afterward.

Wizz app: loading a new hint

Here’s how it looks when the hint is loaded:

Wizz app: displaying a hint for a trivia question

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.