Today, I will do a small demo of the architect of the Flutter project, you can maintain and test it easily.
1. Why do we need an architect in the project?
When I first got into programming, I was only interested in the output and efficiency of the program. I have never structured a program or project that I write.When I come across more projects, projects need to develop new features, fix features, I usually take longer to fix and sometimes also add some bugs to the project. The main cause of all these problems is that I have not followed an architect model. Today, I’m going to start a project to introduce architect in Flutter.
2 Objectives
I will build a project with a girl list screen of items. These items will be taken from the server and taken from the Movie adverb.
3. The architect diagram
Diagram shows how to get data to Data layer and vice versa. Bloc will never refer to the Widget in UI Screen. Screen Screen only observes changes from Bloc class. I will give some Q&A to better understand diagram:
- What is Bloc Pattern? A: It uses state management and accesses data from a central location in Flutter.
- Is this architecture like any other architecture. TL: Same as MVP, MVVM. The only difference is that BLOC will replace ViewModel in MVVM.
- What is under the guise of Bloc? Something essential for state management in one place. TL: STREAMS or REACTIVE is one approach. Data will be transferred from BLOC to UI or from UI to BlOC in stream form. If you haven’t heard about streaming, you can read this post
Start project on Bloc pattern.
- Create a Project Flutter named “myProjectName”.
- Write the following code in the main.dart file:
1 2 3 4 5 6 7 | import 'package:flutter/material.dart'; import 'src/app.dart'; void main(){ runApp(App()); } |
3. Create the app.dart file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import 'package:flutter/material.dart'; import 'ui/movie_list.dart'; class App extends StatelessWidget { @override Widget build(BuildContext context) { // TODO: implement build return MaterialApp( theme: ThemeData.dark(), home: Scaffold( body: MovieList(), ), ); } } |
- Create a new package with the names resources, blocs, models, ui .
- package blocs will contain files related to BLOC.
- Package models will contain POJO (Plain Old Java Object) class or model class of Json response get from server.
- The resources package contains the repository classes and makes network calls.
- package ui contains list of screens shown to the user.
- Add the Rxdart library to the pubspec.yaml file
1 2 3 4 5 6 7 8 9 10 11 | dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 rxdart: ^0.23.1 http: ^0.12.0+4 |
and perform the installation by clicking “Pub get” if you are using Android studio or you type the command in terminal (Mac), command (Win):
1 2 | flutter packages get |
- We are done with the project framework. Now let’s deal with the network layer, understand the API endpoint. You perform login to the page and get API key from Setttings page. We will access the following url to get the data:
1 2 | http://api.themoviedb.org/3/movie/popular?api_key=“your_api_key” |
Response Data:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | { "page": 1, "total_results": 19772, "total_pages": 989, "results": [ { "vote_count": 6503, "id": 299536, "video": false, "vote_average": 8.3, "title": "Avengers: Infinity War", "popularity": 350.154, "poster_path": "/7WsyChQLEftFiDOVTGkv3hFpyyt.jpg", "original_language": "en", "original_title": "Avengers: Infinity War", "genre_ids": [ 12, 878, 14, 28 ], "backdrop_path": "/bOGkgRGdhrBYJSLpXaxhXVstddV.jpg", "adult": false, "overview": "As the Avengers and their allies have continued to protect the world from threats too large for any one hero to handle, a new danger has emerged from the cosmic shadows: Thanos. A despot of intergalactic infamy, his goal is to collect all six Infinity Stones, artifacts of unimaginable power, and use them to inflict his twisted will on all of reality. Everything the Avengers have fought for has led up to this moment - the fate of Earth and existence itself has never been more uncertain.", "release_date": "2018-04-25" }, |
- Now let’s create a model or POJO class for this type of response. Create the file item_model.dart in package models. Copy the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | class ItemModel { int _page; int _total_results; int _total_pages; List<_Result> _results = []; ItemModel.fromJson(Map<String, dynamic> parsedJson) { print(parsedJson['results'].length); _page = parsedJson['page']; _total_results = parsedJson['total_results']; _total_pages = parsedJson['total_pages']; List<_Result> temp = []; for (int i = 0; i < parsedJson['results'].length; i++) { _Result result = _Result(parsedJson['results'][i]); temp.add(result); } _results = temp; } List<_Result> get results => _results; int get total_pages => _total_pages; int get total_results => _total_results; int get page => _page; } class _Result { int _vote_count; int _id; bool _video; var _vote_average; String _title; double _popularity; String _poster_path; String _original_language; String _original_title; List<int> _genre_ids = []; String _backdrop_path; bool _adult; String _overview; String _release_date; _Result(result) { _vote_count = result['vote_count']; _id = result['id']; _video = result['video']; _vote_average = result['vote_average']; _title = result['title']; _popularity = result['popularity']; _poster_path = result['poster_path']; _original_language = result['original_language']; _original_title = result['original_title']; for (int i = 0; i < result['genre_ids'].length; i++) { _genre_ids.add(result['genre_ids'][i]); } _backdrop_path = result['backdrop_path']; _adult = result['adult']; _overview = result['overview']; _release_date = result['release_date']; } String get release_date => _release_date; String get overview => _overview; bool get adult => _adult; String get backdrop_path => _backdrop_path; List<int> get genre_ids => _genre_ids; String get original_title => _original_title; String get original_language => _original_language; String get poster_path => _poster_path; double get popularity => _popularity; String get title => _title; double get vote_average => _vote_average; bool get video => _video; int get id => _id; int get vote_count => _vote_count; } |
- Create file movie_api_provider.dart in package resources to implement the network implementation. Copy and page the following code into the file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import 'dart:async'; import 'package:http/http.dart' show Client; import 'dart:convert'; import '../models/item_model.dart'; class MovieApiProvider { Client client = Client(); final _apiKey = 'your_api_key'; Future<ItemModel> fetchMovieList() async { print("entered"); final response = await client .get("http://api.themoviedb.org/3/movie/popular?api_key=$_apiKey"); print(response.body.toString()); if (response.statusCode == 200) { // If the call to the server was successful, parse the JSON return ItemModel.fromJson(json.decode(response.body)); } else { // If that call was not successful, throw an error. throw Exception('Failed to load post'); } } } |
Note: Please create an account from https://www.themoviedb.org/ and get apikey instead of ‘your_api_key’.
The fetchMovieList method is used to make the API call. When the network call completes it will return a Future ItemModel object if the network call is successful or an exception.
- Next we will create repository.dart into resource . Copy and page the following code into the file:
1 2 3 4 5 6 7 8 9 10 | import 'dart:async'; import 'movie_api_provider.dart'; import '../models/item_model.dart'; class Repository { final moviesApiProvider = MovieApiProvider(); Future<ItemModel> fetchAllMovies() => moviesApiProvider.fetchMovieList(); } |
We will import the movie_api_provider.dart file and call fetchMovieList (). Repository class is the central point from which data will be transferred to BLOC .
- Now for a more complicated part, implement the logic bloc. Let’s create movies_bloc.dart in package blocs . Please copy and page to the movies_bloc.dart file, I will explain in detail:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import '../resources/repository.dart'; import 'package:rxdart/rxdart.dart'; import '../models/item_model.dart'; class MoviesBloc { final _repository = Repository(); final _moviesFetcher = PublishSubject<ItemModel>(); Observable<ItemModel> get allMovies => _moviesFetcher.stream; fetchAllMovies() async { ItemModel itemModel = await _repository.fetchAllMovies(); _moviesFetcher.sink.add(itemModel); } dispose() { _moviesFetcher.close(); } } final bloc = MoviesBloc(); |
Inside the MoviesBloc class, we are creating the Repository class object which will be used to access the fetchAllMovies ()
In the MoviesBloc class, we create an object class Repository used to access fetchAllMovies () by creating a PublishSubject object which is responsible for adding the data it receives from the server as an ItemModel object and converting it to the UI screen as a stream. . To convert ItemModel object as stream we create a method allMovies () whose return type is Observables. Using RxDart makes updating data from server to monitor easy. The UI screen will always observe the MoviesBloc class to listen for changes and update if any.
- At the end of the article, copy the following code to the movie_list.dart file, this file is created from the UI package :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | import 'package:flutter/material.dart'; import '../models/item_model.dart'; import '../blocs/movies_bloc.dart'; class MovieList extends StatelessWidget { @override Widget build(BuildContext context) { bloc.fetchAllMovies(); return Scaffold( appBar: AppBar( title: Text('Popular Movies'), ), body: StreamBuilder( stream: bloc.allMovies, builder: (context, AsyncSnapshot<ItemModel> snapshot) { if (snapshot.hasData) { return buildList(snapshot); } else if (snapshot.hasError) { return Text(snapshot.error.toString()); } return Center(child: CircularProgressIndicator()); }, ), ); } Widget buildList(AsyncSnapshot<ItemModel> snapshot) { return GridView.builder( itemCount: snapshot.data.results.length, gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2), itemBuilder: (BuildContext context, int index) { return Image.network( 'https://image.tmdb.org/t/p/w185${snapshot.data .results[index].poster_path}', fit: BoxFit.cover, ); }); } } |
The above code is not using StatefulWidget, instead will use StreamBuilder instead of updating UI.
Here is the complete code, please refer to it: https://github.com/SAGARSURI/MyMovies .
References:
https://medium.com/codechai/architecting-your-flutter-project-bd04e144a8f1