'Navigate to next question or item in quiz app in a flutter list

I am creating a quiz app in dart/flutter and I want to automatically navigate to the next question when an answer is selected. The questions and answers are in a list. Please someone should point me where I am doing it wrong. Here is my code

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../providers/quiz/question_provider.dart';
import '../providers/quiz/quiz_provider.dart';

import '../widgets/quiz/question_item.dart';


class QuestionScreen extends StatefulWidget {
  static var quizId;
  static const routeName = '/question-screen';

  @override
  State<QuestionScreen> createState() => _QuestionScreenState();
}

class _QuestionScreenState extends State<QuestionScreen> {
  var _isInit = true;
  var _isLoading = false;

  @override
  void didChangeDependencies() {
    if (_isInit) {
      setState(() {
        _isLoading = true;
      });
      Provider.of<Question>(context).fetchQuestions().then((_) {
        setState(() {
          _isLoading = false;
        });
      });
    }
    _isInit = false;
    super.didChangeDependencies();
  }

  // var _questionIndex = 0;

  // void answerQuestion() {
  //   setState(() {
  //     _questionIndex = _questionIndex + 1;
  //   });
  // }

  @override
  Widget build(BuildContext context) {
    QuestionScreen.quizId = ModalRoute.of(context)!.settings.arguments as String;
    final quiz = Provider.of<Quiz>(context).findById(QuestionScreen.quizId);
    final question = Provider.of<Question>(context);
    
    return Scaffold(
      appBar: AppBar(
        title: Text('${quiz.title}'),
      ),
      body: Container(
              padding: EdgeInsets.all(10),
              child: ListView.builder(
                itemCount: 1,
                itemBuilder: (ctx, index) => ChangeNotifierProvider.value(
                  value: question.questions[index],
                  child: question.questions.isEmpty ? Container() : QuestionItem(),
                ),
              ),
            ),
    );
  }
}

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '/providers/quiz/question_provider.dart';
import '/screens/question_answers.dart';
import './question.dart';
import './answer.dart';

class QuestionItem extends StatefulWidget {
  final Function? answerHandler;
  const QuestionItem({Key? key, this.answerHandler}) : super(key: key);

  @override
  State<QuestionItem> createState() => _QuestionItemState();
}

class _QuestionItemState extends State<QuestionItem> {
  @override
  Widget build(BuildContext context) {
    final loadedQuestions = Provider.of<Question>(context);
    var questionIndex = 1;
  
  void answerQuestion() {
    setState(() {
      questionIndex = questionIndex! + 1;
    });
  }
    return GridTile(
      child: GestureDetector(
        onTap: () =>
            Navigator.of(context).pushNamed(QuestionsAndAnswers.routeName),
        child: Column(
          children: [
           QuestionText(
                    questionText: '${loadedQuestions.questionTitle!}'),
            ...(loadedQuestions.options as List<dynamic>).map((option) {
              return Answer(
                    answerText: '${option}',
                    selectHandler: answerQuestion,
                  );
            }).toList()
          ],
        ),
      ),
    );
  }
}

And below is the provider class where I fetch the quiz data from an API and add them to a list.

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:firebase_auth/firebase_auth.dart';

import '/screens/question_screen.dart';

class Question with ChangeNotifier {
  final String? id;
  final String? questionTitle;
  final String? description;
  final String? lessonId;
  final String? courseId;
  final String? quizId;
  final String? media;
  final List<dynamic>? rightAnswers;
  final List<dynamic>? options;
  final int? grade = 1;
  int? index = 0;

  Question({
    @required this.id,
    @required this.questionTitle,
    @required this.description,
    @required this.lessonId,
    @required this.courseId,
    @required this.quizId,
    @required this.media,
    @required this.rightAnswers,
    @required this.options,
    @required this.index,
  });

  List<Question> _questions = [];

  List<Question> get questions {
    return [..._questions];
  }

  Question findById(String id) {
    return _questions.firstWhere((question) => question.id == id);
  }

  Future<void> fetchQuestions() async {
    try {
      final String idToken =
          await FirebaseAuth.instance.currentUser!.getIdToken();

      final reauthUrl = Uri.parse(
          'https://example.net/api/v1/User/reauth');

      final reauthResponse = await http.post(reauthUrl,
          headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json'
          },
          body: json.encode({"token": idToken}));

      final returnedTokenData =
          json.decode(reauthResponse.body) as Map<String, dynamic>;

      final serverToken = returnedTokenData['token'];

      final url = Uri.parse(
          'https://example.net/api/v1/Quiz/Questions/${QuestionScreen.quizId}');

      final response = await http.get(
        url,
        headers: {'Authorization': 'Bearer $serverToken'},
      );

      final responseData = json.decode(response.body);
      print('ResponseData is $responseData');
      final List<Question> loadedQuestions = [];
      for (var question in responseData) {
        loadedQuestions.add(
          Question(
            id: question['id'],
            questionTitle: question['title'], 
            description: question['description'],
            lessonId: question['lessonId'],
            courseId: question['courseId'],
            quizId: question['quizId'],
            media: question['media'],
            rightAnswers: question['rightAnswers'],
            options: question['options'],
          ),
        );
      }
      _questions = loadedQuestions;
      print('These are the questions $_questions');
      notifyListeners();
    } catch (error) {
      rethrow;
    }
  }
}



Solution 1:[1]

I finally figured out a way to navigate to the next question on the quiz.

I moved the questionIndex variable and answerQuestion() function from the QuestionItem class to the QuestionScreen page and passed the function as a parameter in the QuestionItem widget.

I then used question.questions[questionIndex] as the value of ListView.builder(), and voila! It works!

Here is the final code

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../providers/quiz/question_provider.dart';
import '../providers/quiz/quiz_provider.dart';

import '../widgets/quiz/question_item.dart';


class QuestionScreen extends StatefulWidget {
  static var quizId;
  static const routeName = '/question-screen';

  @override
  State<QuestionScreen> createState() => _QuestionScreenState();
}

class _QuestionScreenState extends State<QuestionScreen> {
  var _isInit = true;
  var _isLoading = false;
  var questionIndex = 0;

  @override
  void didChangeDependencies() {
    if (_isInit) {
      setState(() {
        _isLoading = true;
      });
      Provider.of<Question>(context).fetchQuestions().then((_) {
        setState(() {
          _isLoading = false;
        });
      });
    }
    _isInit = false;
    super.didChangeDependencies();
  }

    void answerQuestion() {
      setState(() {
        questionIndex = questionIndex + 1;
      });
    }

  @override
  Widget build(BuildContext context) {
    QuestionScreen.quizId = ModalRoute.of(context)!.settings.arguments as String;
    final quiz = Provider.of<Quiz>(context).findById(QuestionScreen.quizId);
    final question = Provider.of<Question>(context);
    
    return Scaffold(
      appBar: AppBar(
        title: Text('${quiz.title}'),
      ),
      body: Container(
              padding: EdgeInsets.all(10),
              child: ListView.builder(
                itemCount: 1,
                itemBuilder: (ctx, index) => ChangeNotifierProvider.value(
                  value: question.questions[questionIndex],
                  child: question.questions.isEmpty ? Container() : QuestionItem(answerHandler: answerQuestion),
                ),
              ),
            ),
    );
  }
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '/providers/quiz/question_provider.dart';
import '/screens/question_answers.dart';
import './question.dart';
import './answer.dart';

class QuestionItem extends StatefulWidget {
  final VoidCallback answerHandler;
  const QuestionItem({Key? key, required this.answerHandler}) : super(key: key);

  @override
  State<QuestionItem> createState() => _QuestionItemState();
}

class _QuestionItemState extends State<QuestionItem> {
  @override
  Widget build(BuildContext context) {
    final loadedQuestions = Provider.of<Question>(context);

    return GridTile(
      child: GestureDetector(
        onTap: () =>
            Navigator.of(context).pushNamed(QuestionsAndAnswers.routeName),
        child: Column(
          children: [
            QuestionText(questionText: '${loadedQuestions.questionTitle!}'),
            SizedBox(height: 10,),
            ...(loadedQuestions.options as List<dynamic>).map((option) {
              return Answer(
                answerText: '${option}',
                selectHandler: widget.answerHandler,
              );
            }).toList(),
          ],
        ),
      ),
    );
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Cliff