🔮Future data

Future data management using Pulse

Use tip: At first, you'll have to install Pulse-X package. Here's the installation tip.

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.

You'll need to add http package in your pubspec.yaml file to make api requests.

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 TODOand 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