I am developing a mobile application using Flutter. I am using the flutter bloc package, https://pub.dev/packages/flutter_bloc for managing and setting up the bloc. But when the state change it is not updating the widgets or views.
I have a bloc class file called home_bloc.dart with the following implementation.
class HomeEvent {
static const int FETCH_ARTICLES = 1;
static const int TOGGLE_IS_FILTERING = 2;
int _event = 0;
String _filterKeyword = "";
int get event => _event;
void set event(int event) {
this._event = event;
}
String get filterKeyword => _filterKeyword;
void set filterKeyword(String filterKeyword) {
this._filterKeyword = filterKeyword;
}
}
class HomeBloc extends Bloc<HomeEvent, HomeState> {
Repository _repository = Repository();
HomeState state = HomeState();
#override
HomeState get initialState => state;
#override
Stream<HomeState> mapEventToState(HomeEvent event) async* {
switch (event.event) {
case HomeEvent.FETCH_ARTICLES:
{
List<dynamic> articles = List<dynamic>();
fetchArticles(filter: event.filterKeyword).listen((dynamic article) {
articles.add(article);
});
state = state.copyWith(articles: articles);
break;
}
case HomeEvent.TOGGLE_IS_FILTERING:
{
state.isFiltering = ! state.isFiltering;
state = state.copyWith();
break;
}
default:
{
state = state.initial();
break;
}
}
yield state;
}
Stream<dynamic> fetchArticles({String filter = ""}) async* {
List<dynamic> list = (this.state.articles.length > 0)
? this.state.articles
: await _repository.getArticles();
if (filter.isNotEmpty) {
for (var article in list) {
if (article is String) {
yield article;
} else if (article.title.contains(filter)) {
yield article;
}
}
} else {
for (var article in list) {
yield article;
}
}
}
}
class HomeState {
bool _isFiltering = false;
List<dynamic> _articles = List<dynamic>();
bool get isFiltering => _isFiltering;
void set isFiltering(bool isFiltering) {
this._isFiltering = isFiltering;
}
List<dynamic> get articles => _articles;
void set articles(List<dynamic> list) {
this._articles = list;
}
HomeState initial() {
HomeState state = HomeState();
state.isFiltering = false;
state.articles = List<dynamic>();
return state;
}
HomeState copyWith({ bool isFiltering, List<dynamic> articles }) {
HomeState state = HomeState();
state.isFiltering = isFiltering != null? isFiltering: this._isFiltering;
state.articles = articles!=null && articles.length > 0? articles: this._articles;
return state;
}
}
This is my repository class returning dummy data.
class Repository {
Future<List<dynamic>> getArticles() async {
List<dynamic> list = List<dynamic>();
list.add("A");
Article article1 = Article();
article1.id = 1;
article1.title = "A start is born";
list.add(article1);
Article article2 = Article();
article2.id = 2;
article2.title = "Asking for help";
list.add(article2);
Article article3 = Article();
article3.id = 3;
article3.title = "Angel is comming";
list.add(article3);
list.add("B");
Article article4 = Article();
article4.id = 4;
article4.title = "Baby Boss";
list.add(article4);
Article article5 = Article();
article5.id = 5;
article5.title = "Beginner guide to Staying at Home";
list.add(article5);
list.add("C");
Article article6 = Article();
article6.id = 6;
article6.title = "Care each other";
list.add(article6);
Article article7 = Article();
article7.id = 7;
article7.title = "Controlling the world";
list.add(article7);
Article article8 = Article();
article8.id = 8;
article8.title = "Chasing the dream";
list.add(article8);
return list;
}
}
This is my HomePage widget
class HomePage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _HomePageState();
}
}
class _HomePageState extends State<HomePage> {
IconData _searchIcon = Icons.search;
Widget _appBarTitle;
HomeBloc _homeBloc;
#override
void initState() {
super.initState();
this._homeBloc = BlocProvider.of(context);
WidgetsBinding.instance.addPostFrameCallback((_) => this.fetchArticles());
WidgetsBinding.instance
.addPostFrameCallback((_) => this.buildAppBarTitle());
}
#override
Widget build(BuildContext context) {
return BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(title: Text("Home"),),
body: Container(
child: buildListView(context, state),
),
);
},
);
}
#override
void dispose() {
super.dispose();
}
void buildAppBarTitle() {
this.setState(() {
if (_searchIcon == Icons.search) {
this._appBarTitle = Text("Home");
} else {
this._appBarTitle = TextField(
onChanged: (String inputValue) {
debugPrint("Search term has changed $inputValue");
//homeBloc.fetchArticles(filter: inputValue);
},
style: TextStyle(
color: Colors.white,
),
decoration: InputDecoration(
hintText: "Search",
),
);
}
});
}
Widget buildAppBarSearchIcon() {
return IconButton(
icon: Icon(
_searchIcon,
color: Colors.white,
),
onPressed: () {
if (this._searchIcon == Icons.search) {
//display the search text field and close icons
this.setState(() {
this._searchIcon = Icons.close;
this.buildAppBarTitle();
//homeBloc.toggleFiltering();
});
} else {
this.fetchArticles();
this.setState(() {
this._searchIcon = Icons.search;
this.buildAppBarTitle();
//homeBloc.toggleFiltering();
});
}
});
}
Widget buildListView(
BuildContext context, HomeState state) {
if (state.articles.length > 0) {
var listView = ListView.builder(
itemCount: state.articles.length,
itemBuilder: (context, index) {
var item = state.articles[index];
if (item is String) {
return buildListFirstInitialView(item);
}
Article article = item as Article;
return buildListArticleView(article);
});
return listView;
} else {
return Center(
child: Text("No resources found."),
);
}
}
Widget buildListFirstInitialView(String initial) {
return ListTile(
title: Text(initial),
);
}
Widget buildListArticleView(Article article) {
return ListTile(
title: Text(article.title),
);
}
Widget buildBottomNavigationBar() {
return BottomNavigationBar(
currentIndex: 0,
onTap: (int position) {},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
title: Text('Settings'),
),
]);
}
void fetchArticles({String filter = ""}) {
HomeEvent event = HomeEvent();
event.event = HomeEvent.FETCH_ARTICLES;
_homeBloc.add(event);
}
}
As you can see this is my HomePage widget is doing. It will fetch the articles after the widget is built. Then the list view will be updated with the dummy data.
My main.dart file.
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: BlocProvider(
create: (context) => HomeBloc(),
child: HomePage(),
),
);
}
}
When I run my app, it is not updating the list view with the dummy data. Instead, it is always showing the message for no records found.
Why is it not working?
Bloc will never change If the state didn't change, you might be confused that you assigned a state, but the fact is that Bloc is a Stream, you'll need to yield a state instead of assigning It directly.
Hopefully, you had implemented a copyWith, so you could do It as below:
yield state.copyWith(articles: articles)
For keeping your structure, you could still use:
// state = state.copyWith(articles: articles);
newState = state.copyWith(articles: articles);
...
yield newState;
Because state variable is used by Bloc, you must use another variable to prevent Bloc from comparing the same variable (state == state will always true, so the state never changed.)
your state class is not equatable ... Dart can't tell when the state has changed
You must use:
import 'package:equatable/equatable.dart';
I also don't think you should SET the state property on your bloc class. You should only yield it and let the bloc update it...
please check the docs as I could be wrong
I had the same issue and solved this problem changing:
yield status;
to
yield status.toList();
At the end of my mapEventToState() method.
And probably you had to make for all yield that is passing a List.
If it worked for you let me know
Related
i am quite new with flutter. I am trying to add a ChangeNotifierProvider into my app. I use flutter_azure_b2c to log in a user, in order to handle to login outcome I have the following code:
AzureB2C.registerCallback(B2COperationSource.POLICY_TRIGGER_INTERACTIVE,
(result) async {
if (result.reason == B2COperationState.SUCCESS) {
List<String>? subjects = await AzureB2C.getSubjects();
if (subjects != null && subjects.isNotEmpty) {
B2CAccessToken? token = await AzureB2C.getAccessToken(subjects[0]);
if (!mounted || token == null) return;
final encodedPayload = token.token.split('.')[1];
final payloadData =
utf8.fuse(base64).decode(base64.normalize(encodedPayload));
final claims = Claims.fromJson(jsonDecode(payloadData));
var m = Provider.of<LoginModel>(context);
m.logIn(claims);
}
}
});
The problem is that when it arrives to var m = Provider.of<LoginModel>(context); the execution stops with out errors without executing m.logIn(claims);, so the model is not changed and the consumer is not called.
Any idea?
This is my consumer:
class App extends StatelessWidget {
const App({super.key});
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => LoginModel(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: appTheme,
home: Consumer<LoginModel>(
builder: (context, value, child) =>
value.claims != null ? const Home() : const Login(),
)),
);
}
}
class LoginModel extends ChangeNotifier {
Claims? _claims;
logIn(Claims claims) {
_claims = claims;
notifyListeners();
}
logOut() {
_claims = null;
notifyListeners();
}
Claims? get claims => _claims;
}
My LoginWidget:
class Login extends StatefulWidget {
const Login({super.key});
#override
LoginState createState() => LoginState();
}
class LoginState extends State<Login> {
B2CConfiguration? _configuration;
checkLogin(BuildContext context) async {
List<String>? subjects = await AzureB2C.getSubjects();
if (subjects != null && subjects.isNotEmpty) {
B2CAccessToken? token = await AzureB2C.getAccessToken(subjects[0]);
if (!mounted || token == null) return;
final encodedData = token.token.split('.')[1];
final data =
utf8.fuse(base64).decode(base64.normalize(encodedData));
final claims = Claims.fromJson(jsonDecode(data));
var m = Provider.of<LoginModel>(context, listen: true);
m.logIn(claims); //<-- debugger never reaches this line
}
}
#override
Widget build(BuildContext context) {
// It is possible to register callbacks in order to handle return values
// from asynchronous calls to the plugin
AzureB2C.registerCallback(B2COperationSource.INIT, (result) async {
if (result.reason == B2COperationState.SUCCESS) {
_configuration = await AzureB2C.getConfiguration();
if (!mounted) return;
await checkLogin(context);
}
});
AzureB2C.registerCallback(B2COperationSource.POLICY_TRIGGER_INTERACTIVE,
(result) async {
if (result.reason == B2COperationState.SUCCESS) {
if (!mounted) return;
await checkLogin(context);
}
});
// Important: Remeber to handle redirect states (if you want to support
// the web platform with redirect method) and init the AzureB2C plugin
// before the material app starts.
AzureB2C.handleRedirectFuture().then((_) => AzureB2C.init("auth_config"));
const String assetName = 'assets/images/logo.svg';
final Widget logo = SvgPicture.asset(
assetName,
);
return SafeArea(
child: //omitted,
);
}
}
I opened an issue as well, but it did not help me.
Try this
var m = Provider.of<LoginModel>(context, listen: false)._claims;
You are using the Provider syntax but not doing anything really with it. You need to set it like this Provider.of<LoginModel>(context, listen: false).login(claims) and call it like this Provider.of<LoginModel>(context, listen: false)._claims;
I fixed it, moving the callback registrations from the build method to the initState method.
when I add an event from a stateless widget by using BlocProvider.of<>, it really adds event and yield state, and BlocBuilder work and change UI,
But, when adding an event from a separate class, it really adds an event to the bloc and onTransition work, but not yield a new state, and BlocBuilder not work to change UI.
the main :
main(){
return runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
MQTTManager x = MQTTManager();
return MaterialApp(
home: BlocProvider(
lazy: false,
create:(context)=>MqttblocBloc(MQTTManager())..add(StartConnect()) ,
child:Home() ,
),
);
}
}
the bloc :
class MqttblocBloc extends Bloc<MqttblocEvent, MqttblocState> {
MQTTManager manager = MQTTManager() ;
MqttblocBloc(this.manager) : super(MqttblocInitial());
#override
Stream<MqttblocState> mapEventToState(
MqttblocEvent event,
) async* {
if(event is StartConnect){
try{
manager.initializeMQTTClient();
manager.connect();
yield MqttblocInitial();
}catch(e){
print(e);
}
}
else if(event is ConnectedEvent) {
try{
print('inBloc connect....');
yield ConnectedState(MQTTManager.s);
}catch(e){
print(e);
}
}
else if(event is PublishedEvent){
try{
manager.publishsw1('on');
print('inBloc publish........');
yield PublishState(manager.getText1());
}catch(e){
print(e);
}
}
else if(event is DisconnectedEvent) {
try{
print('inBloc And Disconnnnn....');
yield DisconnectState(MQTTManager.u);
}catch(e){
print(e);
}
}
}
#override
void onTransition(Transition<MqttblocEvent, MqttblocState> transition) {
super.onTransition(transition);
print(transition);
}
}
and here separate class where I listen to server and add events to bloc :
class MQTTManager {
MqttblocBloc bloc ;
static var s ;
static var u ;
MqttServerClient client;
String text ;
String text1 ;
String text2 ;
static List<String> conn = [] ;
void initializeMQTTClient(){
client = MqttServerClient("broker.shiftr.io","User");
client.port = 1883;
client.secure = false;
client.logging(on: true);
client.onConnected = onConnected;
final MqttConnectMessage connMess = MqttConnectMessage()
.authenticateAs('889514b9', 'd5459e3f6b0422cb')
.withClientIdentifier("User")
.withWillTopic('willtopic')
.withWillMessage('My Will message')
.startClean() // Non persistent session for testing
.withWillQos(MqttQos.atLeastOnce);
print('EXAMPLE::Mosquitto client connecting....');
client.connectionMessage = connMess;
}
// Connect to the host
void connect() async{
assert(client != null);
try {
print('EXAMPLE::Mosquitto start client connecting....');
await client.connect();
Amar(); // <...... here calling this fun to start listen to Server
} on Exception catch (e) {
print('EXAMPLE::client exception - $e');
disconnect();
}
}
void disconnect() {
print('Disconnected');
client.disconnect();
}
void publishsw1(String message){
final MqttClientPayloadBuilder builder = MqttClientPayloadBuilder();
builder.addString(message);
client.publishMessage('hello/sw1', MqttQos.exactlyOnce, builder.payload);
}
void onConnected() {
print('EXAMPLE::shiftr client connected....');
client.subscribe("hello/sw1", MqttQos.atLeastOnce);
client.updates.listen((List<MqttReceivedMessage<MqttMessage>> c) {
final MqttPublishMessage recMess = c[0].payload;
final String pt =
MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
setText(pt);
});
}
Amar() { //<....... here h listen to server
bloc = MqttblocBloc(this);
client.subscribe("\$events", MqttQos.atLeastOnce);
client.updates.listen((List<MqttReceivedMessage<MqttMessage>> c) {
final MqttPublishMessage recMess = c[0].payload;
final String pt =
MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
var z = DetectEvent.fromJson(json.decode(pt));
if(z.type == 'connected'){
var connected = Connected.fromJson(json.decode(pt));
print (connected.type);
bloc.add(ConnectedEvent()); // <... here I add event to Bloc , but it not changeing UI
}
else if(z.type == 'disconnected'){
var disconnected = Disconnected.fromJson(json.decode(pt));
print (disconnected.type) ;
bloc.add(DisconnectedEvent()); // <... here I add event to Bloc , but it not changeing UI
}
else if(z.type == 'published'){
var published = Published.fromJson(json.decode(pt));
print(published.type) ;
}
}
}
and that is a stateless widget and use blocbuider :
class Home extends StatelessWidget {
MQTTManager v = MQTTManager();
#override
Widget build(BuildContext context) {
MqttblocBloc p = BlocProvider.of<MqttblocBloc>(context);
return Scaffold(
appBar: AppBar(
title: Text('Bloc MQTT'),
actions: [
Row(
children: [
IconButton(
icon: Icon(Icons.wb_incandescent,
),
onPressed: (){
p.add(PublishedEvent());//<.... here it change UI ,
},
),
],
)
],
),
body: Center(
child: BlocBuilder<MqttblocBloc,MqttblocState>(
// buildWhen: (previuosState , currentState)=>currentState.runtimeType !=previuosState.runtimeType,
builder:(context , state){
if(state is MqttblocInitial){
return CircularProgressIndicator();
}
else if(state is ConnectedState){
return IconButton(
icon: Icon(Icons.wifi),
onPressed: (){},
);
}
else if(state is PublishState){
return RaisedButton(
child: Text('${state.x}'),
onPressed: (){},
);
}
else if(state is DisconnectState){
return IconButton(
icon: Icon(Icons.wb_incandescent),
onPressed: (){
},
);
}
return CircularProgressIndicator();
} ,
),
)
);
}
}
bloc State :
#immutable
abstract class MqttblocState extends Equatable {
const MqttblocState();
#override
List<Object> get props => [];
}
class MqttblocInitial extends MqttblocState {}
class ConnectedState extends MqttblocState{
final String x ;
ConnectedState(this.x);
#override
List<Object> get props => [x];
}
class PublishState extends MqttblocState{
final String x ;
PublishState(this.x);
#override
List<Object> get props => [x];
}
class DisconnectState extends MqttblocState{
final String x ;
DisconnectState(this.x);
#override
List<Object> get props => [x];
}
and bloc events
#immutable
abstract class MqttblocEvent extends Equatable {
MqttblocEvent();
#override
List<Object> get props => [];
}
class StartConnect extends MqttblocEvent{}
class ConnectedEvent extends MqttblocEvent{}
class PublishedEvent extends MqttblocEvent{}
class DisconnectedEvent extends MqttblocEvent{}
The UI won't rebuild upon sending the same state again. You need to send some other state first. So for any event, your mapping should look like this:
if(event is StartConnect){
yield MqrrblocInProgress(); // <============== ADDED
try{
manager.initializeMQTTClient();
manager.connect();
yield MqttblocInitial();
}catch(e){
print(e);
}
Of course, you need to define this state (InProgress), too, as well as define some widget in the UI for this state (eg spinning wheel)
here is why, You're yielding the same state and using Equatable without being any different props to compare to so the BlocBuilder is not seeing any change.
You have two solutions ether un-inherit Equatable from class MqttblocEvent:
abstract class MqttblocEvent {
MqttblocEvent();
}
Or yield different state in-between like MqrrblocInProgress (Recommended) :
Stream<MqttblocState> mapEventToState(
MqttblocEvent event,
) async* {
yield MqrrblocInProgress();
.....
Problem
Recursive rendering of the Widget due to incorrect (probably) use of Provider.
This is the main.dart :
void main() {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<NotesProvider>(
create: (_) => NotesProvider(),
),
ChangeNotifierProvider<ThemeProvider>(
create: (_) => ThemeProvider(),
),
],
child: MyApp(),
));
}
This redirects to outer_page which contains two tabs like this :
It's code goes to like this :
class OuterPage extends StatefulWidget {
static const routeName = '/OuterPage';
#override
State<StatefulWidget> createState() {
return OuterPageState();
}
}
class OuterPageState extends State<OuterPage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
int _selectedTab = 0;
final _pageOptions = [
NoteScreen(), // <- Notes Tab
NotePageScreen(), // <- 'Another' Tab
];
Widget build(BuildContext context) {
var noteProvider = Provider.of<NotesProvider>(context, listen: false);
// https://stackoverflow.com/a/53839983
var customFabButton;
if (_selectedTab == 0) {
~~~ SNIP ~~~
The default tab is the 'Notes' Tab, which works fine.
'Another' tab, is where the issue lies.
class NotePageScreen extends StatefulWidget {
NotePageScreen();
#override
NotePageScreenState createState() => NotePageScreenState();
}
class NotePageScreenState extends State<NotePageScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
List<Note> noteList;
int count = 0;
#override
Widget build(BuildContext context) {
Provider.of<NotesProvider>(context, listen: false).getAllDecryptedNotes();
return Scaffold(
key: _scaffoldKey,
body: Provider.of<NotesProvider>(context, listen:false).decrypted
? NotePage()
: Container(
child: Center(
child: Text("Add a new Note"),
),
));
}
}
What's going on here
I am fetching the decrypted notes from the database.
NoteProvider.dart :
class NotesProvider with ChangeNotifier {
DatabaseHelper _databaseHelper = DatabaseHelper();
List<Note> _noteList, decryptedNoteList;
int _count = 0;
bool _notesDecrypted = false;
UnmodifiableListView<Note> get allNotes => UnmodifiableListView(_noteList);
getNotes() async {
await _databaseHelper.initializeDatabase();
List<Note> noteList = await _databaseHelper.getNoteList();
this._noteList = noteList;
this._count = noteList.length;
notifyListeners();
}
UnmodifiableListView<Note> get allDecryptedNotes =>
UnmodifiableListView(decryptedNoteList);
getAllDecryptedNotes() async {
List<Note> decryptedNoteList = [];
for (var note in this._noteList) {
decryptedNoteList.add(await decryptNote(note));
}
this.decryptedNoteList = decryptedNoteList;
this._notesDecrypted = true;
notifyListeners();
}
int get count => _count;
bool get decrypted => _notesDecrypted;
~~~~ SNIP ~~~~
What's the problem here
So, what happens is the first time there isn't any decrypted data, but when I swtich tabs and come back again to 'Another' tab, there are the decrypted notes.
What I've tried :
If I set listen to True on either of these :
Provider.of<NotesProvider>(context, listen: false).getAllDecryptedNotes();
Provider.of<NotesProvider>(context, listen: false).decrypted
then the page loads in the first attempt but then it goes on rendering recursively.
That's where the error is.
Thanks :)
Update - Adding Repo
Repo : https://github.com/LuD1161/notes_app/
Branch : reusable_components
I'm brand new to Flutter / Dart and I'm trying to build a reusable infinite scroller with placeholder loading. The class is as follows:
import 'dart:async';
import 'package:flutter/material.dart';
class PagedScroller<T> extends StatefulWidget {
final int limit;
final Future<List<T>> Function(int, int) getDataFunction;
final Widget Function(T) renderFunction;
final Widget Function() renderPlaceholderFunction;
PagedScroller(
{#required this.limit,
#required this.getDataFunction,
#required this.renderFunction,
#required this.renderPlaceholderFunction});
#override
_PagedScrollerState<T> createState() => _PagedScrollerState<T>();
}
class _PagedScrollerState<T> extends State<PagedScroller> {
int _offset = 0;
int _lastDataLength = 1; // Init to one so the first call can happen
List<dynamic> _items = [];
Future<List<dynamic>> _future;
bool _isInitializing = false;
bool _isInitialized = false;
bool _isLoading = false;
ScrollController _controller =
ScrollController(initialScrollOffset: 0.0, keepScrollOffset: true);
_PagedScrollerState();
void _init() {
_isInitializing = true;
_reset();
_controller.addListener(() {
bool loadMore = false;
if (_controller.position.maxScrollExtent == double.infinity) {
loadMore = _controller.offset == _controller.position.maxScrollExtent;
} else {
loadMore =
_controller.offset >= _controller.position.maxScrollExtent * 0.85;
}
// Only load more if it's not currently loading and we're not on the last page
// _lastDataLength should be 0 if there are no more pages
if (loadMore && !_isLoading && _lastDataLength > 0) {
_offset += widget.limit;
_load();
}
});
_load();
_isInitializing = false;
_isInitialized = true;
}
void _reset() {
// Clear things array and reset inital get-things link (without paging)
setState(() {
_future = _clearThings();
});
// Reload things
// Reset to initial GET link
_offset = 0;
}
void _load() {
setState(() {
_future = _loadPlaceholders();
_future = _loadData();
});
}
Future<List<dynamic>> _clearThings() async {
_items.clear();
return Future.value(_items);
}
Future<List<dynamic>> _loadPlaceholders() async {
// Add 20 empty placeholders to represent stuff that's currently loading
for (var i = 0; i < widget.limit; i++) {
_items.add(_Placeholder());
}
return Future.value(_items);
}
List<dynamic> _getInitialPlaceholders() {
var placeholders = List<dynamic>();
for (var i = 0; i < widget.limit; i++) {
placeholders.add(_Placeholder());
}
return placeholders;
}
Future<List<dynamic>> _loadData() async {
_setLoading(true);
var data = await widget.getDataFunction(widget.limit, _offset);
// When loading data is done, remove any placeholders
_items.removeWhere((item) => item is _Placeholder);
// If 0 items were returned, it's probably the last page
_lastDataLength = data.length;
for (var item in data) {
_items.add(item);
}
_setLoading(false);
return Future.value(_items);
}
void _setLoading(bool isLoading) {
if (!mounted) {
return;
}
setState(() {
_isLoading = isLoading;
});
}
Future<void> _refreshThings() async {
_reset();
_load();
return Future;
}
#override
Widget build(BuildContext context) {
if (!_isInitializing && !_isInitialized) {
_init();
}
return FutureBuilder(
future: _future,
initialData: _getInitialPlaceholders(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
List<dynamic> loadedItems = snapshot.data;
return RefreshIndicator(
onRefresh: _refreshThings,
child: ListView.builder(
itemCount: loadedItems.length,
controller: _controller,
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
var item = loadedItems[index];
if (item is _Placeholder) {
return widget.renderPlaceholderFunction();
} else if (item is T) {
// THIS IS THE LINE THAT FAILS
return widget.renderFunction(item);
}
return Text('Unknown item type');
},
),
);
}
return Container();
},
);
}
}
class _Placeholder {}
The line that fails above:
return widget.renderFunction(item);
Fails with the following:
type '(MyModel) => Widget' is not a subtype of type '(dynamic) => Widget'
I understand why this is happening. The compiler can't know that type T from my PagedScroller<T> is the same as type T from _PagedScrollerState<T>. As a result, Dart tries to be helpful and converts my callback function of type Widget Function(T) to Widget Function(dynamic).
I then figured "maybe I can fake it out" with the following since I know the T in PagedScroller<T> and _PagedScrollerState<T> are always the same:
var renderFunction = widget.renderFunction as Widget Function(T);
return renderFunction(item);
Interestingly, this gives me a warning:
Unnecessary cast.
Try removing the cast.
Yet it won't even run that line (crashes) with the following:
Either the assertion indicates an error in the framework itself, or we should provide substantially more information in this error message to help you determine and fix the underlying cause.
In either case, please report this assertion by filing a bug on GitHub:
https://github.com/flutter/flutter/issues/new?template=BUG.md
Changing everything to dynamic works a charm, but I really don't want to lose the readability of generics here if I don't have to.
Despite extensive searching, I can't find the equivalent of C#'s Convert.ChangeType where you can provide types at runtime so I can just do the cast I want and be done with it.
This seems like a really simple thing to achieve, but I'm stuck.
You can consume the scroller with this simple main.dart copy/pasted:
import 'package:flutter/material.dart';
import 'package:minimal_repros/paged_scroller.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
Future<List<MyModel>> getDataFunction(int limit, int offset) async {
var myModels = List<MyModel>();
// Simulate API call
await Future.delayed(Duration(milliseconds: 1000));
for (int i = 0; i < limit; i++) {
var myModel = MyModel();
myModel.count = i + offset;
myModel.firstName = 'Bob';
myModels.add(myModel);
}
return myModels;
}
Widget renderFunction(MyModel myModel) {
return Text(myModel.firstName);
}
Widget renderPlaceholderFunction() {
return Text('Loading');
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: PagedScroller(
getDataFunction: getDataFunction,
renderFunction: renderFunction,
renderPlaceholderFunction: renderPlaceholderFunction,
limit: 20));
}
}
class MyModel {
int count;
String firstName;
}
In the declaration of your State class, you forgot to specify the generic parameter of the widget.
Instead of:
class _PagedScrollerState<T> extends State<PagedScroller> {
do:
class _PagedScrollerState<T> extends State<PagedScroller<T>> {
in flutter i just learn how can i use Bloc on applications and i want to try to implementing simple login with this feature. after implementing some class of bloc to using that on view
i get error when i try to use this code as
BlocProvider.of<LoginListingBloc>(context).dispatch(LoginEvent(loginInfoModel: testLogin));
inside RaisedButton
Error:
BlocProvider.of() called with a context that does not contain a Bloc
of type LoginListingBloc.
My view :
class _HomePageState extends State<HomePage> {
LoginListingBloc _loginListingBloc;
#override
void initState() {
super.initState();
_loginListingBloc =
LoginListingBloc(loginRepository: widget.loginRepository);
}
...
#override
Widget build(BuildContext context) {
return BlocProvider(
bloc: _loginListingBloc,
child: Scaffold(
appBar: AppBar(
elevation: 5.0, title: Text('Sample Code', style: appBarTextStyle)),
body: Center(
child: RaisedButton(
child: Text(
'click here',
style: defaultButtonStyle,
),
onPressed: () {
BlocProvider.of<LoginListingBloc>(context).dispatch(LoginEvent(loginInfoModel: testLogin));
}),
),
),
);
}
}
LoginListingBloc class:
class LoginListingBloc extends Bloc<LoginListingEvent, LoginListingStates> {
final LoginRepository loginRepository;
LoginListingBloc({this.loginRepository});
#override
LoginListingStates get initialState => LoginUninitializedState();
#override
Stream<LoginListingStates> mapEventToState(
LoginListingStates currentState, LoginListingEvent event) async* {
if (event is LoginEvent) {
yield LoginFetchingState();
try {
final loginInfo = await loginRepository.fetchLoginToPage(
event.loginInfoModel.username, event.loginInfoModel.password);
yield LoginFetchedState(userInfo: loginInfo);
} catch (_) {
yield LoginErrorState();
}
}
}
}
and other classes if you want to see theme
AppApiProvider class:
class AppApiProvider {
final successCode = 200;
Future<UserInfo> fetchLoginToPage(String username, String password) async {
final response = await http.get(Constants.url + "/api/v1/getPersons");
final responseString = jsonDecode(response.body);
if (response.statusCode == successCode) {
print(responseString);
return UserInfo.fromJson(responseString);
} else {
throw Exception('failed to get information');
}
}
}
LoginEvent:
class LoginEvent extends LoginListingEvent {
final LoginInfoModel loginInfoModel;
LoginEvent({#required this.loginInfoModel}) : assert(loginInfoModel != null);
}
LoginInfoModel:
class LoginInfoModel {
String username;
String password;
LoginInfoModel({this.username, this.password});
}
final testLogin = LoginInfoModel(username:'exmaple',password:'text');
No need to access loginListingBloc from context since it exists in the current class and not up the widget tree.
change:
BlocProvider.of<LoginListingBloc>(context).dispatch(LoginEvent(loginInfoModel: testLogin));
to:
_loginListingBloc.dispatch(LoginEvent(loginInfoModel: testLogin));
For all others who come here for the error message:
Make sure you always specify the types and don't omit them:
BlocProvider<YourBloc>(
create: (context) => YourBloc()
child: YourWidget()
);
and also for
BlocProvider.of<YourBloc>(context);