Implementing API in Flutter Using MVVM Architecture - Practical Guide
Flutter, a popular UI toolkit, enables developers to build natively compiled applications for mobile, web, and desktop from a single codebase. When it comes to fetching and managing data from APIs, adopting a well-structured architecture is essential. In this blog post, we’ll explore how to implement an API in Flutter using the Model-View-ViewModel (MVVM) architecture, a design pattern that promotes separation of concerns and maintainability.
Note: This is a practical guide. We will not cover the theoretical definition. We will focus on coding part only,
MVVM Architecture Overview:
MVVM is a design pattern that divides an application into three main components:
- Model: Represents the data and business logic of the application. In our case, it involves handling data fetched from the Marvel API.
- View: Represents the UI elements and their layout. In Flutter, this corresponds to the widgets that make up the user interface.
- ViewModel: Acts as an intermediary between the Model and the View. It transforms the data from the Model into a format that the View can use and updates the Model based on user interactions with the View.
Introduction:
In this tutorial, we are going to create a Flutter app to display a list of Marvel superheroes. We’ll implement the MVVM architecture and try to follow best practices to keep our code clean, readable, and maintainable.
Let’s start a practical guide
Step 1: Setting Up the Project
Let’s create a new project using this terminal command. You also can create a new project using your IDE.
flutter create marvel_api_flutter
Step 2: Adding Dependencies
We need to use these dependencies in our project:
- http: A composable, Future-based library for making HTTP requests. Please check the latest version here.
- provider: A wrapper around InheritedWidget to make it easier to use and more reusable. The Provider package is often used in Flutter applications to manage state and provide a convenient way to propagate changes through the widget tree. Please check for more details and the latest version here.
Open your project in any preferred IDE. I’m using Android Studio for my project. Now open the pubspec.yaml
file and add the following dependencies:
dependencies:
flutter:
sdk: flutter
#library for making HTTP requests
http: ^1.1.2
#library for reuse or init ViewModel
provider: ^6.1.1
Run flutter pub get
to fetch the dependencies.
Step 3: Creating data models for API data
Create a superhero_model.dart
file to define the structure of the superhero data:
class SuperHero {
final String? name;
final String? realName;
final String? imageUrl;
SuperHero({this.name, this.realName, this.imageUrl});
factory SuperHero.fromJson(Mapdynamic> json) => SuperHero(
name: json["name"],
realName: json["realname"],
imageUrl: json["imageurl"],
);
}
Step 4: Create a Service Class
Now, create a api_service.dart
service class to handle the API calls. Service class responsible for making network requests
import 'package:http/http.dart' as http;
class ApiService {
final String apiUrl = "https://www.simplifiedcoding.net/demos/marvel";
Future<http.Response> fetchSuperheroes() async {
return await http.get(Uri.parse(apiUrl));
}
}
Step 5: Create a Repository Class
Create a repository.dart
repository to abstract the data source.
import 'dart:convert';
import 'package:marvel_api_flutter/api_service.dart';
import 'package:marvel_api_flutter/superhero_model.dart';
class SuperheroRepository {
final ApiService _service = ApiService();
Future<List<SuperHero>> getSuperheroes() async {
final response = await _service.fetchSuperheroes();
if (response.statusCode == 200) {
return List<SuperHero>.from(
json.decode(response.body).map((x) => SuperHero.fromJson(x)));
} else {
throw Exception('Failed to load superheroes');
}
}
}
Step 6: Implementing ViewModel
Create a superhero_view_model.dart
ViewModel
import 'package:marvel_api_flutter/repository.dart';
import 'package:marvel_api_flutter/superhero_model.dart';
class SuperheroViewModel extends ChangeNotifier {
final SuperheroRepository _repository = SuperheroRepository();
List<SuperHero> _superheroes = [];
bool fetchingData = false;
List<SuperHero> get superheroes => _superheroes;
Future<void> fetchSuperheroes() async {
fetchingData = true;
try {
_superheroes = await _repository.getSuperheroes();
notifyListeners();
} catch (e) {
throw Exception('Failed to load superheroes: $e');
}
fetchingData = false;
}
}
Step 7: Creating the View
Now, create a superhero_view.dart
file to define the UI. Here, we have 3 classes for a complete list view and a detail screen. You can create separate files for each class.
import 'package:flutter/material.dart';
import 'package:marvel_api_flutter/superhero_model.dart';
import 'package:marvel_api_flutter/superhero_view_model.dart';
import 'package:provider/provider.dart';
import 'character_detail_screen.dart';
class SuperheroView extends StatelessWidget {
const SuperheroView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Marvel Superheroes'),
),
body: Consumer<SuperheroViewModel>(builder: (context, viewModel, child) {
if (!viewModel.fetchingData && viewModel.superheroes.isEmpty) {
Provider.of<SuperheroViewModel>(context, listen: false)
.fetchSuperheroes();
}
if (viewModel.fetchingData) {
// While data is being fetched
return const LinearProgressIndicator();
} else {
// If data is successfully fetched
List<SuperHero> heroes = viewModel.superheroes;
return Column(
children: [
Flexible(
child: ListView.builder(
itemCount: heroes.length,
itemBuilder: (context, index) {
return ListCard(character: heroes[index]);
},
))
],
);
}
}),
floatingActionButton: FloatingActionButton(
onPressed: () {
Provider.of<SuperheroViewModel>(context, listen: false)
.fetchSuperheroes();
},
child: Icon(Icons.refresh),
),
);
}
}
//class for List Card
class ListCard extends StatelessWidget {
const ListCard({super.key, required this.character});
final SuperHero character;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
CharacterDetailScreen(characterDetail: character)));
},
child: Container(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.lightBlue.shade50,
borderRadius: BorderRadius.circular(16)),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image(
image: NetworkImage(character.imageUrl ?? ""),
height: 100,
width: 100,
fit: BoxFit.fitHeight,
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
character.name ?? "",
style: const TextStyle(
color: Colors.black,
fontSize: 24,
fontWeight: FontWeight.bold),
textAlign: TextAlign.start,
),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
character.realName ?? "",
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w400),
))
],
))
],
),
),
),
));
}
}
// class for hero detail screen
class CharacterDetailScreen extends StatelessWidget {
const CharacterDetailScreen({super.key, required this.characterDetail});
final SuperHero characterDetail;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(characterDetail.name ?? "")),
body: Container(
decoration: const BoxDecoration(color: Colors.white),
child: Image(
image: NetworkImage(characterDetail.imageUrl ?? ""),
),
));
}
}
Step 8: Wrap the App with a ChangeNotifierProvider
Update your main.dart
use ChangeNotifierProvider
. Replace the content of the main.dart
file with the following:
import 'package:flutter/material.dart';
import 'package:marvel_api_flutter/superhero_view.dart';
import 'package:marvel_api_flutter/superhero_view_model.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => SuperheroViewModel(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Marvel API Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SuperheroView(),
);
}
}
That’s it. Finally, run the app using flutter run
. You will see a list of Marvel superheroes fetched from the provided API using MVVM architecture.
Conclusion:
By implementing the MVVM architecture in your Flutter app, you can achieve a clean and maintainable codebase. The separation of concerns provided by MVVM enhances code readability, testability, and scalability. This tutorial serves as a starting point for integrating APIs in Flutter while following best practices in app architecture. Feel free to extend and customize the code to suit your project requirements.
Thank you for taking the time to read our blog! We appreciate your interest and hope you found the information helpful.