Example of a ChangeNotifier class that can be used as an architecture in a Flutter app
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.