Skip to content

Instantly share code, notes, and snippets.

@aquiseb
Last active October 12, 2019 14:23
Show Gist options
  • Select an option

  • Save aquiseb/6c84ef1311ca2d2be75344dd0a254f4f to your computer and use it in GitHub Desktop.

Select an option

Save aquiseb/6c84ef1311ca2d2be75344dd0a254f4f to your computer and use it in GitHub Desktop.
Flutter bloc form rxdart api requests
import 'package:flutter/material.dart';
/* MODEL packages */
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
/* BLoC packages */
import 'package:rxdart/rxdart.dart';
/* Provider packages */
// Will import the model and the bloc if separated file
/* Main packages */
// Will import model, bloc and provider
/* MODEL start */
class Movie {
final String title, posterPath, overview;
// Pass the values to the constructor
Movie(this.title, this.posterPath, this.overview);
// Parse the json response
Movie.fromJson(Map json)
: title = json['title'],
posterPath = json["poster_path"],
overview = json["producer"];
}
// Used to query the API
class API {
final http.Client _client = http.Client();
// The url to query. A function will concat this url with a string that the user typed in
static const String _url = 'https://swapi.co/api/films/';
// A method called get that will output a future list of movie types
Future<List<Movie>> get(String query) async {
// We start of by defining an empty list
List<Movie> list = [];
await _client
.get(Uri.parse(query != null
? (_url + query)
: _url)) // add the user input query to the url
.then((res) => res.body)
.then(json.decode) // decode the result
.then((json) {
var results = List();
if (json["results"] is List)
// Multiple results because no user input
results = json["results"];
else
print("Single result");
// Single result due to user input
results.add(json);
return results;
}) // old: json["results"]) // get everything that's inside the results key
.then((movies) => movies.forEach((movie) => list.add(
Movie.fromJson(movie)))); // Add each movie to our list of movies
return list;
}
}
/* MODEL end */
/* BLoC start */
class MovieBloc {
// Define a global variable which will be our API
final API api;
// STREAMS EMITTED FROM THE BLOC
// Define the streams that we will be using inside of this bloc
// These to streams will be originating from the bloc and passed into our app
Stream<List<Movie>> _results = Stream.empty();
Stream<String> _log = Stream.empty();
// STREAMS BROUGHT INTO THE BLOC
// Can be replaced by a a rxdart StreamController
// ReplaySubject allows us to admit the observer of all items that were admitted from the source regardless of when we subscribe to it
// Allows us to delay when we want to admit the query to our function
// The user will be typing into a text box and we'll be able to delay the time when we need it
ReplaySubject<String> _query = ReplaySubject<String>();
// GETTERS TO FETCH THESE STREAMS FROM OUTSIDE OF THE BLOC
// Sink is like the stream's destination. It allows us to get the end of stream where the data is going
// We can grab the very front of our query stream by getting this sink of the stream
Stream<String> get log => _log;
Stream<List<Movie>> get results => _results;
Sink<String> get query => _query;
// CONSTRUCTOR FOR OUR BLOC
// Allows us to define how theses streams will interact with each-other
MovieBloc(this.api) {
// distinct: allows us to specify that we only want the events from the stream that are distinct
// - If there are multiple events that are exactly the same, we will throw them out
// - When the user is typing into the text box, if he stops typing, we'll have the same event over and over again
// - As we don't want to continuously ping to the api, so we can throw theses events out thanks to distinct
// asyncMap: enables to map to our observable or stream, asynchronously
// asBroadcastStream: we want the stream to be a broadcast, meaning that it allows multiple subscribers
_results = _query.distinct().asyncMap(api.get).asBroadcastStream();
// Observable: we istanciate a new observable around our results stream
// withLatestFrom: then we get the latest piece of data from our results Observable
// - we take the string that the user is typing into the text box
// - pass it to the anonymous function `(_, query) => 'Results for $query'` on our query.stream, and put it into the string
// -> everytime the user types a character in, it will print it out
// asBroadcastStream: same reasons as above
_log = Observable(results)
.withLatestFrom(_query.stream, (_, query) => 'Results for $query')
.asBroadcastStream();
}
void dispose() {
// As streams don't automatically dispose in dart, we need to get rid of the query stream
// If we leave them open it can cause memory leaks
_query.close();
// We don't need to dispose the other two streams (_log and _results)
// because they are originating from our bloc instead of coming into our bloc
// _query on the other hand is coming from the UI into our bloc
}
}
/* BLoC end */
/* PROVIDER start */
// Used to serve the bloc to our app
class MovieProvider extends InheritedWidget {
// global MovieBloc
final MovieBloc movieBloc;
// We have it always return true
@override
bool updateShouldNotify(InheritedWidget oldWidget) => true;
// Static method called of. It allows us to easily gain access to the bloc anywhere in our app
// Inside of it we just call context.inherit... and pass in our Provider. We cast this as a MovieProvider. And we'll get the actual movieBloc out.
static MovieBloc of(BuildContext context) =>
(context.inheritFromWidgetOfExactType(MovieProvider) as MovieProvider)
.movieBloc;
// Constructor. Having an optional key, optional movieBloc, and optional child
MovieProvider({Key key, MovieBloc movieBloc, Widget child})
// we take our movieBloc and define it as the movieBloc that inside of our widget
// if it's null, we instantiate a new bloc with an API inside of it
: this.movieBloc = movieBloc ?? MovieBloc(API()),
// we pass our child and key to the super constructor
super(child: child, key: key);
}
/* PROVIDER end */
/* MAIN start */
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Thanks to the provider, we can access our streams from anywhere in the app
// We can access it with `final movieBloc = MovieProvider.of(context);`
return MovieProvider(
// Optionally, we could pass a new MovieBloc and the API instantiation here
// However, as it was defined in the constructor of our provider, this is not necessary
// movieBloc: MovieBloc(API()),
child: MaterialApp(
title: 'RXDART DEMO',
theme: ThemeData(),
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final movieBloc = MovieProvider.of(context);
return Scaffold(
appBar: AppBar(
title: Text("Bloc example"),
),
body: Column(
children: <Widget>[
Container(
padding: EdgeInsets.all(10.0),
child: TextField(
// we get movieBloc, grab our query stream and call add on it
// add: will add the text fields data into our sink inside of our bloc
onChanged: movieBloc.query.add,
decoration: InputDecoration(
border: OutlineInputBorder(),
hintText: "Enter a number between 1 and 7",
),
),
),
// will be responsible for the log stream
StreamBuilder(
// we first take the movieBloc.log stream
stream: movieBloc.log,
// we take the string that we got back from the log stream
// pass it through the snapshot
builder: (context, snapshot) => Container(
// reference the data, or pass empty string if it's null
child: Text(snapshot?.data ?? ''),
),
),
// Flexible because we will have a list inside of it
Flexible(
child: StreamBuilder(
stream: movieBloc.results,
// results movies stored into the asynchronous snapshot
builder: (context, snapshot) {
if (!snapshot.hasData)
return Center(
child: CircularProgressIndicator(),
);
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) => ListTile(
// leading: CircleAvatar(
// child: Image.network(
// "https://image.tmdb.org/t/p/w92" +
// snapshot.data[index].posterPath,
// ),
// ),
title: Text(snapshot.data[index].title),
subtitle: Text(snapshot.data[index].overview),
),
);
},
),
),
],
),
);
}
}
/* MAIN end */
@aquiseb

aquiseb commented Oct 12, 2019

Copy link
Copy Markdown
Author

Add this to your pubspec.yaml

http: ^0.12.0+2
rxdart: ^0.22.3

Replace your main.dart with the above code. And you're good to go.

This gist is based on the full tutorial of Tensor Development Using the BloC Pattern to Build Reactive Applications with Streams in Dart's Flutter Framework.

This gist uses the starwars api instead of themoviesdb.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment