The MVVM (Model-View-ViewModel) architecture is a popular architectural pattern used in many modern software applications. It is used to separate the concerns of the presentation layer (UI) from the business logic and data access layers.
In Flutter, the MVVM architecture can be implemented using various approaches, but the most common one is by using the provider package. The provider package allows for easy implementation of the MVVM pattern and is widely used in the Flutter community.
Here is a brief overview of how MVVM architecture is used in Flutter:
- Model: This represents the data that the application works with. In Flutter, models can be created using plain Dart classes or by using packages such as JSON Serializable or Freezed. These packages help to generate boilerplate code for the model classes.
- View: This represents the UI of the application. In Flutter, the view is implemented using widgets. The widgets can be organized into pages or screens, and each widget is responsible for rendering a portion of the UI.
- ViewModel: This represents the bridge between the view and the model. The ViewModel contains the business logic of the application and is responsible for communicating with the model to get or set data. The ViewModel also exposes data to the view through observable variables and functions.
The provider package in Flutter provides a simple way to implement the MVVM architecture. It allows for the creation of a separate class for the ViewModel, which can then be used by the view to retrieve data and manage state.
In summary, the MVVM architecture is a popular pattern in Flutter, and it can be easily implemented using the provider package. It helps to separate the concerns of the UI from the business logic and data access layers, making the code more modular and easier to maintain.the MVVM architecture would help to separate the concerns of the UI from the business logic and data access layers, making the code more modular and easier to maintain. The ViewModel layer would contain the core logic of the application, allowing the view layer to focus on displaying data and the model layer to focus on data storage and retrieval.
1) FOR MODEL:
class Quiz {
final String id;
final String title;
final List<Question> questions;
Quiz({required this.id, required this.title, required this.questions});
}
class Question {
final String id;
final String text;
final List<String> answerChoices;
final int correctAnswer;
Question({
required this.id,
required this.text,
required this.answerChoices,
required this.correctAnswer,
});
}
2.FOR VIEW:
class QuizPage extends StatelessWidget {
final String quizId;
QuizPage({required this.quizId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Quiz'),
),
body: Consumer<QuizViewModel>(
builder: (context, quizViewModel, child) {
final quiz = quizViewModel.getQuizById(quizId);
return Column(
children: [
Text(
quiz.title,
style: TextStyle(fontSize: 20),
),
SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: quiz.questions.length,
itemBuilder: (context, index) {
final question = quiz.questions[index];
return QuestionWidget(
question: question,
onAnswerSelected: (answer) {
quizViewModel.answerQuestion(quizId, question.id, answer);
},
);
},
),
),
ElevatedButton(
onPressed: () {
quizViewModel.submitQuiz(quizId);
},
child: Text('Submit Quiz'),
),
],
);
},
),
);
}
}
class QuestionWidget extends StatefulWidget {
final Question question;
final void Function(int) onAnswerSelected;
QuestionWidget({required this.question, required this.onAnswerSelected});
@override
_QuestionWidgetState createState() => _QuestionWidgetState();
}
class _QuestionWidgetState extends State<QuestionWidget> {
int _selectedAnswerIndex = -1;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.question.text),
SizedBox(height: 10),
...widget.question.answerChoices.map((answerChoice) {
final index = widget.question.answerChoices.indexOf(answerChoice);
return RadioListTile(
title: Text(answerChoice),
value: index,
groupValue: _selectedAnswerIndex,
onChanged: (value) {
setState(() {
_selectedAnswerIndex = value!;
});
widget.onAnswerSelected(value);
},
);
}).toList(),
Divider(),
],
);
}
}
3.FOR VIEWMODEL
class QuizViewModel extends ChangeNotifier {
final QuizRepository _quizRepository;
final UserRepository _userRepository;
Map<String, List<int>> _userAnswersByQuizId = {};
List<Quiz> get quizzes => _quizRepository.getAllQuizzes();
Quiz getQuizById(String quizId) => _quizRepository.getQuizById(quizId);
int getScoreForQuiz(String quizId) {
final userAnswers = _userAnswersByQuizId[quizId] ?? [];
if (userAnswers.isEmpty) {
return 0;
}
final quiz = _quizRepository.getQuizById(quizId);
final correctAnswers = quiz.questions.map((question) => question.correctAnswer).toList();
int score = 0;
for (int i = 0; i < userAnswers.length; i++) {
if (userAnswers[i] == correctAnswers[i]) {
score++;
}
}
return score;
}
void answerQuestion(String quizId, String questionId, int answerIndex) {
final userAnswers = _userAnswersByQuizId[quizId] ?? [];
final questionIndex = getQuizById(quizId).questions.indexWhere((question) => question.id == questionId);
if (questionIndex != -1) {
userAnswers[questionIndex] = answerIndex;
} else {
userAnswers.add(answerIndex);
}
_userAnswersByQuizId[quizId] = userAnswers;
notifyListeners();
}
void submitQuiz(String quizId) async {
final user = _userRepository.getCurrentUser();
if (user == null) {
return;
}
final score = getScoreForQuiz(quizId);
await _quizRepository.submitQuizScore(user.uid, quizId, score);
_userAnswersByQuizId.remove(quizId);
notifyListeners();
}
QuizViewModel({required QuizRepository quizRepository, required UserRepository userRepository})
: _quizRepository = quizRepository,
_userRepository = userRepository;
}
Finally,MVVM promotes a clear separation of concerns, improves testability and maintainability, and supports flexibility and reusability in the codebase.