Last active
October 12, 2019 14:23
-
-
Save aquiseb/6c84ef1311ca2d2be75344dd0a254f4f to your computer and use it in GitHub Desktop.
Flutter bloc form rxdart api requests
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 */ |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this to your pubspec.yaml
Replace your
main.dartwith 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.