Example of a ChangeNotifier class that can be used as an architecture in a Flutter app

Santhosh Adiga U
5 min readMar 3, 2023

--

First, let’s start by adding the http package to your Flutter project by adding the following line to your pubspec.yaml file:

dependencies:
http: ^0.13.4

Then, you can create a model class that will hold the data that you want to fetch using HTTP. For example:

import 'package:flutter/material.dart';

class Post with ChangeNotifier {
int id;
String title;
String body;

Post({this.id, this.title, this.body});

factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}

Next, you can create a service class that will handle the HTTP request and return a list of posts. For example:

import 'dart:convert';

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

import 'post.dart';

class PostService with ChangeNotifier {
List<Post> _posts = [];

List<Post> get posts => _posts;

Future<void> fetchPosts() async {
try {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
final List<dynamic> responseData = json.decode(response.body);
final List<Post> fetchedPosts = responseData.map((post) => Post.fromJson(post)).toList();

_posts = fetchedPosts;

notifyListeners();
} catch (error) {
print(error);
}
}
}

In the fetchPosts method, we're making an HTTP GET request to the JSONPlaceholder API to fetch a list of posts. Once the response is received, we're decoding the JSON data and mapping it to a list of Post objects. Finally, we're setting the _posts property with the fetched data and calling notifyListeners() to notify any listeners that the data has been updated.

Now, in your Flutter app, you can use the Provider package to provide the PostService to any widgets that need access to the fetched data. For example:

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

import 'post_service.dart';
import 'post.dart';

class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final postService = Provider.of<PostService>(context);

return Scaffold(
appBar: AppBar(
title: Text('My App'),
),
body: ListView.builder(
itemCount: postService.posts.length,
itemBuilder: (context, index) {
final post = postService.posts[index];
return ListTile(
title: Text(post.title),
subtitle: Text(post.body),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
postService.fetchPosts();
},
child: Icon(Icons.refresh),
),
);
}
}

In this example, we’re using Provider.of<PostService>(context) to retrieve the PostService instance and access the list of posts. We're also using a ListView.builder to display the list of posts as a series of ListTile widgets, and a FloatingActionButton to trigger the fetchPosts method when it's pressed.

That’s it! You can now use this approach to fetch data from HTTP endpoints and update your app’s state using the notifyListeners() method.

Now, lets create generic abstract class that can be used as an architecture in a Flutter app:

abstract class BaseRepository<T> {
Future<T> fetch();
}

abstract class BaseViewModel<T> with ChangeNotifier {
final BaseRepository<T> repository;
T _data;

BaseViewModel(this.repository);

T get data => _data;

Future<void> fetchData() async {
_data = await repository.fetch();
notifyListeners();
}
}

In this example, we have two abstract classes: BaseRepository and BaseViewModel.

BaseRepository is a generic abstract class that defines a single abstract method fetch(). This method is responsible for fetching data from a data source (such as an API or a database) and returning it.

BaseViewModel is also a generic abstract class that extends ChangeNotifier. It has a reference to an instance of BaseRepository and a private field _data that holds the fetched data. It provides a getter method data to access the fetched data. It also defines a method fetchData() that calls the fetch() method on the repository and sets the fetched data to the _data field. Finally, it calls notifyListeners() to notify any listeners that the data has been updated.

Using these two abstract classes, you can create concrete implementations for specific types of data. For example, you can create a PostRepository that extends BaseRepository<Post> and a PostViewModel that extends BaseViewModel<Post>. Here's an example:

class Post {
final int id;
final String title;
final String body;

Post({this.id, this.title, this.body});
}

class PostRepository extends BaseRepository<Post> {
@override
Future<Post> fetch() async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));
final responseData = json.decode(response.body);
return Post(
id: responseData['id'],
title: responseData['title'],
body: responseData['body'],
);
}
}

class PostViewModel extends BaseViewModel<Post> {
PostViewModel() : super(PostRepository());
}

In this example, we have a Post class that represents a single post. We also have a PostRepository class that extends BaseRepository<Post> and implements the fetch() method to fetch a single post from the JSONPlaceholder API. Finally, we have a PostViewModel class that extends BaseViewModel<Post> and sets the PostRepository instance in the constructor.

You can then use the PostViewModel in your Flutter app to fetch and display a single post. For example:

class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My App'),
),
body: Center(
child: Consumer<PostViewModel>(
builder: (context, viewModel, _) {
if (viewModel.data == null) {
return CircularProgressIndicator();
} else {
final post = viewModel.data;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(post.title),
SizedBox(height: 8),
Text(post.body),
],
);
}
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Provider.of<PostViewModel>(context, listen: false).fetchData();
},
child: Icon(Icons.refresh),
),
);
}
}

In this example, we’re using Consumer<PostViewModel> to listen to changes in the PostViewModel and display the fetched data. We're also using a CircularProgressIndicator while the data is being fetched. Finally, we have a FloatingActionButton that calls fetchData() on the PostViewModel instance when pressed.

This is just an example of how you can use the BaseRepository and BaseViewModel abstract classes as a starting point for your Flutter app architecture. You can extend these classes and add your own methods and fields to suit your specific needs.

For example : Here’s an example implementation of the BaseRepository class for managing CRUD operations on a list of posts:

abstract class BaseRepository<T> {
Future<T> create(T item);
Future<T> read(dynamic id);
Future<T> update(T item);
Future<void> delete(dynamic id);
Future<List<T>> readAll();
}

In this example, we have a Post class that represents a single post. We also have a PostRepository class that extends BaseRepository<Post> and implements all CRUD operations for managing a list of posts. The create(), read(), update(), and readAll() methods manipulate a list of in-memory Post objects, while the delete() method removes an item from the list by its ID.

We also have a toJson() method in the Post class that converts a Post object into a JSON object, allowing us to easily serialize Post objects to send them to a server or store them in a database.

You can create similar concrete implementations of BaseRepository for other types of data and data sources.

--

--

Santhosh Adiga U
Santhosh Adiga U

Written by Santhosh Adiga U

Founder of Anakramy ., dedicated to creating innovative AI-driven cybersecurity solutions.

No responses yet