🔮Future data
Future data management using Pulse
Usage
Pulse-X gives you full control of Future data with PulseXFutureViewModel, PulseXFutureBuilder, PulseXState and PulseXReactions.
PulseXFutureViewModel gives you features such as api handling, api data state control and business logic which are separated from UI.
PulseXFutureBuilder makes UI reactive. It takes view model and builder function as parameters. You can show various widgets based on Future State which can be called via
viewModel.status
.PulseXState is a simple class that has four states - initial,loading, loaded,error. You can control each of them.
PulsXeReactions are side effects inspired from Mobx state management. In Pulse, you can only use side effects with Future data. 🥲
For example project, we'll use dummy data from https://dummyjson.com.
First of all, create a Post
data model.
import 'dart:convert';
Post postsFromJson(String str) => Post.fromJson(json.decode(str));
String postsToJson(Post data) => json.encode(data.toJson());
class Post {
int id;
String title;
String body;
int userId;
List<String> tags;
int reactions;
Post({
required this.id,
required this.title,
required this.body,
required this.userId,
required this.tags,
required this.reactions,
});
factory Post.fromJson(Map<String, dynamic> json) => Post(
id: json["id"],
title: json["title"],
body: json["body"],
userId: json["userId"],
tags: List<String>.from(json["tags"].map((x) => x)),
reactions: json["reactions"],
);
Map<String, dynamic> toJson() => {
"id": id,
"title": title,
"body": body,
"userId": userId,
"tags": List<dynamic>.from(tags.map((x) => x)),
"reactions": reactions,
};
}
Then, create an abstract service class so that you can easily attach, detach and switch concrete services.
import 'package:http/http.dart' as http;
import 'package:posts/model/post.dart';
const String baseUrl = 'https://dummyjson.com';
abstract class ApiService {
final http.Client client = http.Client();
final postUrl = Uri.parse('$baseUrl/posts');
Future<List<Post>> getPosts();
}
After that, create a concrete service class that extends ApiService
class.
const int okStatusCode = 200;
const int notFoundStatusCode = 404;
class PostService extends ApiService {
@override
Future<List<Post>> getPosts() async {
try {
http.Response res = await client.get(postUrl);
if (res.statusCode == okStatusCode) {
dynamic data = res.body as dynamic;
final jsonData = jsonDecode(data);
List<dynamic> list = jsonData['posts'] as List<dynamic>;
List<Post> posts = [];
for (dynamic d in list) {
posts.add(Post.fromJson(d));
}
return posts;
} else if (res.statusCode == notFoundStatusCode) {
throw Exception('Data not found!');
}
throw Exception('Something went wrong!');
} on HttpException catch (ex) {
throw const HttpException('No internet connection!');
} catch (ex) {
rethrow;
}
}
}
Now, you are ready to create View Model that extends PulseXFutureViewModel
.
class PostViewModel extends PulseXFutureViewModel {
final ApiService service;
PostViewModel({required this.service});
@override
void onInit() {
super.onInit();
_fetchPosts();
}
void _fetchPosts() async {
changeState(PulseXState.loading()); // set state to loading
try {
List<Post> posts = await service.getPosts(); // fetch data
changeState(PulseXState.loaded(
posts,
)); // set state to success and pass the data
} catch (ex) {
changeState(PulseXState.error(ex.toString())); // set state to error and return error message
}
}
}
In UI, you have to take care of two things - reactions and future data. First I'll extract widgets as classes to be clear.
ErrorMessage widget class
class ErrorMessage extends StatelessWidget {
const ErrorMessage({Key? key, required this.message}) : super(key: key);
final String message;
@override
Widget build(BuildContext context) {
return Center(
child: Text(
message,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.red.shade500,
),
),
);
}
}
Here, I'll show you one thing. If you don't wanna pass your ViewModel down the widget tree, Pulse-X provides you with PulseXStateManager.of<YourViewModel>(context) method.
PostList widget class
class PostList extends StatelessWidget {
const PostList({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final viewModel = PulseXStateManager.of<PostViewModel>(context); // get your view model
PulseXState? state = viewModel?.value;
List<Post> posts = state?.value as List<Post>;
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
return Container(
width: double.maxFinite,
decoration: BoxDecoration(
color: Colors.amber.shade200,
borderRadius: BorderRadius.circular(8.0),
),
padding: const EdgeInsets.all(
8.0,
),
margin: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 6.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
posts[index].title,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
posts[index].body,
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
));
},
);
}
}
Then, show data based on your state.
class PostView extends StatefulWidget {
const PostView({Key? key}) : super(key: key);
@override
State<PostView> createState() => _PostViewState();
}
class _PostViewState extends State<PostView> {
final postViewModel = PostViewModel(service: PostService()); // initialize your view model
late PulseXReaction reaction;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// TODO: add your reaction here!
}
@override
void dispose() {
reaction.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Posts'),
),
body: PulseFutureBuilder(
viewModel: postViewModel,
builder: (_, PulseXState<dynamic> state, __) {
if (state.status == PulseXStatus.error) { // error state
return ErrorMessage(message: '${state.message}');
} else if (state.status == PulseXStatus.loaded) { // loaded state
return const PostList();
}
return const Center( // loading or initial state
child: CupertinoActivityIndicator(
radius: 20,
color: Colors.amber,
),
);
},
),
);
}
}
As the last step, you only need to create a reaction based on state. If state is loaded, then show a snackbar.
Remove TODO
and add this in your didChangeDependencies()
method.
reaction = PulseXReaction(postViewModel, (value, dispose) {
if (value.status == PulseXStatus.loaded) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Center(
child: Text(
'🥳 Hooray!!, Posts have been loaded!',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.indigo,
),
),
),
backgroundColor: Colors.indigo.shade100,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
),
padding: const EdgeInsets.symmetric(
vertical: 14.0,
horizontal: 8.0,
),
margin: const EdgeInsets.symmetric(
horizontal: 14.0,
vertical: 10.0,
),
behavior: SnackBarBehavior.floating,
showCloseIcon: true,
),
);
}
});
👏🏻 Tada! Now, you understand how pulse works and will be able to use it in your projects.
Complete source code can be found here. https://github.com/YeLwinOo-Steve/posts
Last updated