Flutter tutorial: Lập trình Flutter với Server API
Tiếp theo trong Flutter tutorial này, Báo Flutter xin gửi đến các bạn bài viết chi tiết về Lập trình Flutter với Server API.
Rất nhiều dự án Flutter sử dụng Server APIs để lấy dữ liệu thông qua http communication, ví dụ dưới đây là một thể loại điển hình.
Bài viết mới nhất về hướng dẫn chi tiết làm việc với API : click tại đây
Có thể rất nhiều khái niệm mà bạn không biết trong lập trình Flutter với Server APIs nhưng bạn sẽ sáng tỏ sau khi làm theo Báo Flutter trong bài viết này:
Bài viết này sẽ hướng dẫn bạn tạo ra một ứng dụng hiển thị các bộ phim được xem nhiều nhất và chi tiết từng bộ phim đó.
Các bước chính khi làm việc với Server APIs trong Flutter:
+ Lấy Api Url( Server API link), lấy file JSON
+ Tạo file model.dart từ cấu trúc của file JSON
+ Tạo file APIProvider.dart
+ Tạo file repository.dart
+ Tạo file bloc
+ Thiết lập : UI
Bạn không hiểu phải không ? Vậy là đúng ý của Báo Flutter rồi. Tôi muốn các bạn có thật nhiều thắc mắc và làm theo hướng dẫn của tôi bạn sẽ vỡ ra nhiều thứ và hiểu hơn.
Lấy Api Url( Server API link), lấy file JSON
Để có 1 link Server API và thì không quá khó, bạn có thể dùng Jsonplaceholder để có thể có một file JSON từ thư mục Resources. Nhưng trong bài này Báo Flutter muốn bạn làm quen với thực tế – lấy API key sau khi đăng kí để lấy Server API của một Website.
– Truy cập TheMovieDB – để đăng kí tài khoản.
– Sau khi đăng kí xong, chọn mục Setting để đăng kí nhận API key :
Sau đó click vào API bên trái và click mục đăng kí : Request an API key
Các mục trong phần đăng kí, bạn có thể điền tự do, trừ email: điền đúng email của bạn.
Sau khi đăng kí xong, click vào mục API bên tay trái và kéo xuống mục API Key (v3 auth). Vậy là bạn có API key và có thể lấy Data từ Server.
Để có thể test API key của bạn bằng việc mở đường link từ mục: Example API Request, chứa nội dung của các field trong 1 file JSON.
+ Tạo file model.dart từ cấu trúc của file JSON
Sau khi đã lấy được API key và mở link : https://api.themoviedb.org/3/movie/popular?api_key=”api key của bạn”, bạn có thể thấy file JSON như bên dưới:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
{"page":1, "total_results":10000, "total_pages":500, "results":[ {"popularity":505.531, "vote_count":3119, "video":false, "poster_path":"\/xBHvZcjRiWyobQ9kxBhO6B2dtRI.jpg", "id":419704, "adult":false, "backdrop_path":"\/5BwqwxMEjeFtdknRV792Svo0K1v.jpg", "original_language":"en", "original_title":"Ad Astra", "genre_ids":[18,878], "title":"Ad Astra", "vote_average":6, "overview":"The near future, a time when both hope and hardships drive humanity to look to the stars and beyond. While a mysterious phenomenon menaces to destroy life on planet Earth, astronaut Roy McBride undertakes a mission across the immensity of space and its many perils to uncover the truth about a lost expedition that decades before boldly faced emptiness and silence in search of the unknown.", "release_date":"2019-09-17"}, |
– Vậy là chúng ta đã lấy được data, bây giờ là thời gian viết ứng dụng : Hiển thị những phim phổ biến gần đây.
– tạo Flutter project mới, có tên : basic_server_api
– Trong file main.dart, xoá hết và nhập đoạn code bên dưới dù báo lỗi:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget{ @override Widget build(BuildContext context) { // TODO: implement build return MaterialApp( theme: ThemeData.dark(); home: Scaffold( body: MovieList(), ), ); } } |
– tiếp theo tạo các packages, như bên dưới :
– trong package: models, tạo file item_model.dart. Xuất phát từ form JSON trên, chúng ta tạo các class như bên dưới:
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 |
class ItemModel { int _page; int _total_results; int _total_pages; List<_Result> _results =[] ; // Get data from Json ItemModel.fromJson(Map<String, dynamic> parsedJson){ _page = parsedJson['page']; _total_results = parsedJson['total_results']; _total_pages = parsedJson['total_pages']; for (int i= 0; i< parsedJson['results'].length; i++) { _Result result = _Result(parsedJson['results'][i]); _results.add(result); } } // getter int get page => _page; int get total_results => _total_results; int get total_pages => _total_pages; List<_Result> get results => _results; } class _Result { double _popularity; int _vote_count; bool _video; String _poster_path ; int _id; bool _adult; String _backdrop_path; String _original_language; String _original_title; List<int> _genre_ids=[]; String _title; String _vote_average; String _overview; String _release_date; // getters _Result(result){ _popularity = result['popularity']; _vote_count = result['vote_count']; _video = result['video']; _poster_path = result['poster_path']; _id = result['id']; _adult = result['adult']; _backdrop_path = result['backdrop_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]); } _title = result['title']; _vote_average = result['vote_average'].toString(); _overview = result['overview']; _release_date = result['release_date']; } // getter double get popluarity => _popularity; int get vote_count => _vote_count; bool get video => _video; String get poster_path => _poster_path; int get id => _id; bool get adult => _adult; String get backdrop_path => _backdrop_path; String get original_language => _original_language; String get original_title => _original_title; List<int> get genre_ids => _genre_ids; String get title => _title; String get vote_average => _vote_average; String get overview => _overview; String get release_date => _release_date; } |
* Tạo file APIProvider.dart
– Sau khi tạo xong model, tạo file : movie_api_provider.dart trong package: resource, để tạo phương thức để lấy data (ItemModel) từ Server.
– Để làm việc này, chúng ta sẽ sử dụng thư viện: http => mở file pubspec.yaml , thêm : http: ^0.12.0+1, sau đó nhấn : “Packages get”.
– Thêm đoạn code bên dưới :
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 |
import 'dart:convert'; import 'package:basic_server_api/models/item_model.dart'; import 'package:http/http.dart' show Client, Response;</code> class MovieApiProvider { // Cần import 'package:http/http.dart' show Client Client client = Client (); final _apiKey = 'api Key của bạn '; final _baseUrl = "http://api.themoviedb.org/3/movie"; Future fetchMovieList() async { // cần add : import 'package:http/http.dart' show Response; Response response ; response = await client.get ("$_baseUrl/popular?api_key=$_apiKey"); if(response.statusCode == 200){ // cần khai báo thư viện : import 'dart:convert'; return ItemModel.fromJson(json.decode(response.body)); } else { throw Exception(" Lỗi khi load Json"); } } } |
* Tạo file repository.dart
Trong package : resource, tạo file : repository.dart để load ItemModel- sử dụng provider : MovieApiModel.
1 2 3 4 5 6 7 8 9 10 |
import 'package:basic_server_api/models/item_model.dart'; import 'movie_api_provider.dart'; class Repository { final movieApiProvider = MovieApiProvider(); Future<ItemModel> fetchAllMovies() => movieApiProvider.fetchMovieList(); } |
* Tạo bloc
Từ package : blocs, tạo movies_bloc.dart, file này giúp load data lên UI. Bloc sử dụng rx dart để sử lý các stream data.
Trong file: pubspec.yaml , add : rxdart: ^0.24.0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import '../resources/repository.dart'; import 'package:rxdart/rxdart.dart'; import '../models/item_model.dart'; class MoviesBloc { final _repository = Repository(); // Tạo sink, cần import: rx dart final _moviesFetcher = PublishSubject(); // Tạo Stream Stream <ItemModel> get allMovies => _moviesFetcher.stream; fetchAllMovies() async { ItemModel itemModel = await _repository.fetchAllMovies(); _moviesFetcher.sink.add(itemModel); } dispose(){ _moviesFetcher.close(); } } |
* Thiết lập: UI
Tạo file movie_list.dart, để tạo giao diện hiển thị 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 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 |
import 'package:basic_server_api/blocs/movies_bloc.dart'; import 'package:flutter/material.dart'; import 'package:basic_server_api/models/item_model.dart'; class MovieList extends StatefulWidget{ @override State createState() { // TODO: implement createState return _MovieListState(); } } class _MovieListState extends State { final bloc = MoviesBloc(); @override void initState() { // TODO: implement initState super.initState(); // load data to Bloc bloc.fetchAllMovies(); } @override void dispose() { // TODO: implement dispose super.dispose(); // đóng bloc bloc.dispose(); } @override Widget build(BuildContext context) { // TODO: implement build return Scaffold( appBar: AppBar( title: Text("Phim phổ biến"), ), // get data từ stream body: StreamBuilder( stream: bloc.allMovies, builder: (context, AsyncSnapshot snapshot){ if(snapshot.hasData){ return buildList(snapshot); } else if (snapshot.hasError) return Text(snapshot.error.toString()); // show loading data trước khi get data return Center(child: CircularProgressIndicator()); }, ), ); } Widget buildList (AsyncSnapshot snapshot) { return GridView.builder( itemCount: snapshot.data.results.length, gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), itemBuilder: (BuildContext context, int index){ return GridTile( child: InkResponse( enableFeedback: true, child: Image.network('https://image.tmdb.org/t/p/w185${snapshot.data .results[index].poster_path}', fit: BoxFit.cover, ), onTap: () {}, ), ); }, ); } } |
Dưới đây là kết quả :
Đó là kết quả sau khi lấy dữ liệu từ server và hiển thị. Còn click vào từng ảnh và hiển thị thông tin thì sao ?
Tương tự như những bước trên, ta cũng cần phải làm các bước:
+ Lấy mẫu form JSON
+ Tạo model
+ Tạo provider
+ Tạo repository
+ Tạo bloc
+ Tạo Ui
+ Lấy mẫu form JSON
Server Api link của các film : http://api.themoviedb.org/3/movie/”id của item_model”?api_key=”api key của bạn”
Form của Json :
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 |
{"adult":false, "backdrop_path":"/5BwqwxMEjeFtdknRV792Svo0K1v.jpg", "belongs_to_collection":null, "budget":87500000, "genres":[{"id":878,"name":"Science Fiction"}, {"id":18,"name":"Drama"}], "homepage":"https://www.foxmovies.com/movies/ad-astra", "id":419704, "imdb_id":"tt2935510", "original_language":"en", "original_title":"Ad Astra", "overview":"The near future, a time when both hope and hardships drive humanity to look to the stars and beyond. While a mysterious phenomenon menaces to destroy life on planet Earth, astronaut Roy McBride undertakes a mission across the immensity of space and its many perils to uncover the truth about a lost expedition that decades before boldly faced emptiness and silence in search of the unknown.", "popularity":505.531, "poster_path":"/xBHvZcjRiWyobQ9kxBhO6B2dtRI.jpg", "production_companies":[{"id":490, "logo_path":null, "name":"New Regency Productions", "origin_country":""}, {"id":79963, "logo_path":null, "name":"Keep Your Head", "origin_country":""}, {"id":73492, "logo_path":null, "name":"MadRiver Pictures", "origin_country":""}, {"id":81, "logo_path":"/8wOfUhA7vwU2gbPjQy7Vv3EiF0o.png", "name":"Plan B Entertainment", "origin_country":"US"}, {"id":30666, "logo_path":"/g8LmDZVFWDRJW72sj0nAj1gh8ac.png", "name":"RT Features", "origin_country":"BR"}, { "id":30148, "logo_path":"/zerhOenUD6CkH8SMgZUhrDkOs4w.png", "name":"Bona Film Group", "origin_country":"CN"},{"id":22213, "logo_path":"/qx9K6bFWJupwde0xQDwOvXkOaL8.png", "name":"TSG Entertainment", "origin_country":"US"}], "production_countries":[{"iso_3166_1":"BR","name":"Brazil"},{"iso_3166_1":"CN","name":"China"}, {"iso_3166_1":"US","name":"United States of America"}], "release_date":"2019-09-17", "revenue":127175922, "runtime":123, "spoken_languages":[{"iso_639_1":"en","name":"English"},{"iso_639_1":"no","name":"Norsk"}], "status":"Released", "tagline":"The answers we seek are just outside our reach", "title":"Ad Astra", "video":false, "vote_average":6.0, "vote_count":3123} |
* Tạo model cho detail item
– Trong package: models, tạo file : movie_detail_model.dart
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 |
class MovieDetailModel { int _id; List<_Result> _results= []; MovieDetailModel.fromJson(Map<String, dynamic> parsedJson){ _id = parsedJson['id']; for (int i=0; i<parsedJson['results'].length; i++) { _Result result = _Result(parsedJson['results'][i]); _results.add(result); } } // getters List<_Result> get results => _results; int get id => _id; } class _Result { String _id; String _iso_639_1; String _iso_3166_1; String _key; String _name ; String _site; int _size; String _type; _Result(result){ _id = result['id']; _iso_639_1 = result['iso_639_1']; _iso_3166_1 = result['iso_3166_1']; _key = result['key']; _name = result['name']; _site = result['site']; _size = result['size']; _type = result['type']; } String get id => _id; String get iso_639_1 => _iso_639_1; String get iso_3166_1 => _iso_3166_1; String get key => _key; String get name => _name; String get site => _site; int get size => _size; String get type => _type; } |
+ Tạo provider
Trong file : movie_api_provider.dart, chúng ta thêm hàm fetchMovieDetail():
1 2 3 4 5 6 7 8 9 10 |
Future fetchTrailer(int movieId) async { final response = await client.get("$_baseUrl/$movieId/videos?api_key=$_apiKey");</code> if (response.statusCode == 200) { return MovieDetailModel.fromJson(json.decode(response.body)); } else { throw Exception('Lỗi khi load'); } } |
+ Tạo repository
Thêm line trong file repository.dart:
1 |
Future fetchMovieDetail(int movieId) => movieApiProvider.fetchMovieDetail(movieId); |
+ Tạo bloc
Trong package: blocs, tạo thêm file : movie_detail_bloc.dart
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 |
import 'package:basic_server_api/models/movie_detail_model.dart'; import 'package:basic_server_api/resources/repository.dart'; import 'package:rxdart/rxdart.dart'; import 'package:flutter/material.dart'; class MovieDetailBloc {</code> final _repository = Repository(); final _movieId = PublishSubject(); final _movieDetail = BehaviorSubject<Future>(); Function(int) get fetchMovieDetailById => _movieId.sink.add; Stream <Future> get movieDetail => _movieDetail.stream; MovieDetailBloc() { _movieId.stream.transform(_itemTransformer()).pipe(_movieDetail); } dispose() async { _movieId.close(); await _movieDetail.drain(); _movieDetail.close(); } _itemTransformer(){ return ScanStreamTransformer( (Future movieDetail, int id, int index){ movieDetail = _repository.fetchMovieDetail(id); return movieDetail; } ); } } class MovieDetailBlocProvider extends InheritedWidget { final MovieDetailBloc bloc; MovieDetailBlocProvider({Key key, Widget child}) : bloc = MovieDetailBloc(), super(key: key, child: child); @override bool updateShouldNotify(_) { return true; } static MovieDetailBloc of(BuildContext context) { return (context.inheritFromWidgetOfExactType(MovieDetailBlocProvider) as MovieDetailBlocProvider) .bloc; } } |
+ Tạo file Movie Detail UI:
Trong file : movie_detail.dart
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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
import 'dart:async';</code> import 'package:flutter/material.dart'; import '../models/movie_detail_model.dart'; import '../blocs/movie_detail_bloc.dart'; class MovieDetail extends StatefulWidget { final posterUrl; final description; final releaseDate; final String title; final String voteAverage; final int movieId; MovieDetail({ this.title, this.posterUrl, this.description, this.releaseDate, this.voteAverage, this.movieId, }); @override State createState() { return MovieDetailState( title: title, posterUrl: posterUrl, description: description, releaseDate: releaseDate, voteAverage: voteAverage, movieId: movieId, ); } } class MovieDetailState extends State { final posterUrl; final description; final releaseDate; final String title; final String voteAverage; final int movieId; MovieDetailBloc bloc; MovieDetailState({ this.title, this.posterUrl, this.description, this.releaseDate, this.voteAverage, this.movieId, }); @override void didChangeDependencies() { bloc = MovieDetailBlocProvider.of(context); bloc.fetchMovieDetailById(movieId); print("recreated"); super.didChangeDependencies(); } @override void dispose() { bloc.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( top: false, bottom: false, child: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverAppBar( expandedHeight: 200.0, floating: false, pinned: true, elevation: 0.0, flexibleSpace: FlexibleSpaceBar( background: Image.network( "https://image.tmdb.org/t/p/w500$posterUrl", fit: BoxFit.cover, )), ), ]; }, body: ListView( children: [ Padding( padding: const EdgeInsets.all(10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container(margin: EdgeInsets.only(top: 5.0)), Text( title, style: TextStyle( fontSize: 25.0, fontWeight: FontWeight.bold, ), ), Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)), Row( children: [ Icon( Icons.favorite, color: Colors.red, ), Container( margin: EdgeInsets.only(left: 1.0, right: 1.0), ), Text( voteAverage, style: TextStyle( fontSize: 18.0, ), ), Container( margin: EdgeInsets.only(left: 10.0, right: 10.0), ), Text( releaseDate, style: TextStyle( fontSize: 18.0, ), ), ], ), Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)), Text(description), Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)), Text( "Trailer", style: TextStyle( fontSize: 25.0, fontWeight: FontWeight.bold, ), ), Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)), StreamBuilder( stream: bloc.movieDetail, builder: (context, AsyncSnapshot<Future> snapshot) { if (snapshot.hasData) { return FutureBuilder( future: snapshot.data, builder: (context, AsyncSnapshot itemSnapShot) { if (itemSnapShot.hasData) { if (itemSnapShot.data.results.length > 0) return trailerLayout(itemSnapShot.data); else return noTrailer(itemSnapShot.data); } else { return Center( child: CircularProgressIndicator()); } }, ); } else { return Center(child: CircularProgressIndicator()); } }, ), ], ), ), ], )), ), ); } Widget noTrailer(MovieDetailModel data) { return Center( child: Container( child: Text("No trailer available"), ), ); } Widget trailerLayout(MovieDetailModel data) { if (data.results.length > 1) { return Row( children: [ trailerItem(data, 0), trailerItem(data, 1), ], ); } else { return Row( children: [ trailerItem(data, 0), ], ); } } trailerItem(MovieDetailModel data, int index) { return Expanded( child: Column( children: [ Container( margin: EdgeInsets.all(5.0), height: 100.0, color: Colors.grey, child: Center(child: Icon(Icons.play_circle_filled)), ), Text( data.results[index].name, maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ); } } |
– tiếp theo đó , chúng ta cần tích hợp trong màn hình chính , để có thể click vào từng item để xem chi tiết.
Trong file: movie_list.dart, trong hàm : buildList , mục onTap , thêm dòng code :
1 |
onTap: () => openDetailPage(snapshot.data, index), |
và viết hàm : openDetailPage ();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
openDetailPage(ItemModel data, int index) { final page = MovieDetailBlocProvider( child: MovieDetail( title: data.results[index].title, posterUrl: data.results[index].backdrop_path, description: data.results[index].overview, releaseDate: data.results[index].release_date, voteAverage: data.results[index].vote_average.toString(), movieId: data.results[index].id, ), ); Navigator.push( context, MaterialPageRoute(builder: (context) { return page; }), ); } |
Vậy là đã xong, dưới đây là kết qủa sau khi click vào item :
Quá dài phải không các bạn ? Lại còn khó hiểu nữa ? Tôi muốn các bạn có thật nhiều thắc mắc.
Và sẽ đặt nhiều câu hỏi, khi đó bạn có thể hiểu đầy đủ về bloc ,kết nối API Server.
Tất cả đều được giải thích trong blog này của tôi.
Để hiểu thêm về các khái niệm, các bạn có thể đọc lại kiến thức về Bloc – trong chương IV, kiến thức về: lập trình bất đồng bộ trong chương I.
Nếu các bạn có lỗi khi chạy , bạn có thể tham khảo source code trong file Github của tôi .
Chúc các bạn có được nhiều kiến thức từ bài viết của tôi !
* Có một số nội dung được tham khảo từ các bài báo nước ngoài.
anh ơi, em lấy trên github của anh về. Run lên thì nó chỉ chạy vào trường hợp trả về circular thôi ạ. Nó k show list, em cám ơn
Chào em, dấu hiệu như vậy có nghĩa là : em chưa load data được từ internet
Code a đưa lên Github chưa có API Key nên không thể load data được.
Vì vậy, em cần đăng kí trên trang đó và lấy API Key và thử lại nhé
Chúc em may mắn !
Exception: Bad state: Insecure HTTP is not allowed by platform
Unhandled Exception: type ‘Future’ is not a subtype of type ‘Future’
Của em báo 2 lỗi như này và app chỉ nhảy vào đoạn load chứ không hiện phim lên
Em đã điền key riêng của em chưa ?
rồi ạ, em đoán là luồng chạy của nó không tốt (Bad state) nên là nó cứ vào cái progesbar suốt
Non-nullable instance field ‘_total_pages’ must be initialized.
Try adding an initializer expression, or add a field initializer in this constructor, or mark it ‘late’.dartnot_initialized_non_nullable_instance_field
<<< Hi anh, ở model item bị lỗi như thế này ạ ? Anh có gặp lỗi này chưa ?
Cái đó em phải xử lý nullsafety em ah
Em đang học về cách ghép api và em bị lỗi này:
type ‘PublishSubject’ is not a subtype of type ‘Stream’
Em phải sửa như nào ạ, em cảm ơn.