FutureBuilder The method 'findRenderObject' was called on null - flutter

I want to render the position of a RichText built by a FutureBuilder as the code below, I used the WidgetsBinding.instance.addPostFrameCallback in the initState() but I got an error: The method 'findRenderObject' was called on null., I tried this approach without FutureBuilder works fine, I do not know how to solve this with FutureBuilder
class BookScreen extends StatefulWidget {
int bookId;
BookScreen(this.bookId);
#override
_BookScreenState createState() => _BookScreenState();
}
class _BookScreenState extends State<BookScreen> {
final GlobalKey _itemKey = GlobalKey();
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {findRichText();});
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: Provider.of<Book>(context, listen: false)
.getBookDetail(widget.bookId),
builder: (ctx, snapshot) => snapshot.connectionState == ConnectionState.waiting
? Center(
child: CircularProgressIndicator(),
)
: ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(10.0),
child: RichText(
key: _itemKey, // Here is the global key
text: TextSpan(
children: _getTextSpan(snapshot.data),
),
),
),
],
),
);
void findRichText() {
var richText = _itemKey.currentContext.findRenderObject() as RenderParagraph;
print(richText.localToGlobal(Offset.zero));
}

It is possible to query the text position after it renders.
For example, you can move ListView to a separate widget. When postframe callback is called, the text will already exist so you'll get its position
class _BookScreenState extends State<BookScreen> {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: ...,
builder: (ctx, snapshot) =>
snapshot.connectionState == ConnectionState.waiting
? Center(child: CircularProgressIndicator())
: BooksList(data: snapshot.data),
);
}
}
class BooksList extends StatefulWidget {
final BooksListData data;
BooksList({#required this.data});
#override
_BooksListState createState() => _BooksListState();
}
class _BooksListState extends State<BooksList> {
final GlobalKey _itemKey = GlobalKey();
#override
Widget build(BuildContext context) {
return ListView(
children: [
RichText(
key: _itemKey,
text: TextSpan(
children: _getTextSpan(widget.data),
),
),
],
);
}
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
findRichText();
});
}
void findRichText() {
var richText = _itemKey.currentContext.findRenderObject() as RenderParagraph;
print(richText.localToGlobal(Offset.zero));
}
}
However this approach complicates the code and doesn't seem reliable.
Alternatively, if you want scrolling to listview item, you can use scrollable_positioned_list package. It provides more declarative api:
final ItemScrollController itemScrollController = ItemScrollController();
ScrollablePositionedList.builder(
itemCount: ...,
itemBuilder: (context, index) => ...,
itemScrollController: itemScrollController,
);
itemScrollController.jumpTo(
index: 100,
alignment: 0.5,
);

Related

disable button with press but without rebuilding the whole screen

I have a quiz screen where I am using an API with FutureBuilder. Each time build method is refreshed, the new question is fetched. There's a submit button at the bottom to save the response and reset the screen. What I want to do is to disable the submit button until new question is fetched after pressing the submit button and make enabled when new question is rebuild. I cannot call the setstate to make it null with a bool variable because new question is loaded due to this. Here's my code to reproduce the issue:
import 'package:flutter/material.dart';
class QuizForm extends StatefulWidget {
const QuizForm({Key? key}) : super(key: key);
#override
State<QuizForm> createState() => _QuizFormState();
}
class _QuizFormState extends State<QuizForm> {
int buildCount = 0 ;
getQuestion () {}
#override
Widget build(BuildContext context) {
print(buildCount);
print('Question Fetched and UI is building');
return SafeArea(child: Scaffold(
body: FutureBuilder(
future: getQuestion(),
builder: (context, snapshot){
return ListView(
children: [
ListTile(title: Text('Quiz Title'),),
ListTile(title: Text('1'),),
ListTile(title: Text('2'),),
ListTile(title: Text('3'),),
ListTile(title: Text('4'),),
SizedBox(height: 20,),
ElevatedButton(
onPressed: () async {
print('Please Wait, Answer is getting Saved');
// Button Should be shown disabled for 3 seconds
await Future.delayed(const Duration(seconds: 3));
buildCount++;
setState(() {
// this setState rebuilds the screen and new question is loaded
// because of future builder
});
}, child: Text('Submit Quiz'))
],
);
},
),
));
}
}
When you are getting data from API check if you have data in your variable , if has data return data if not then call API ,
update : with _submitEnabled value .
Here example :
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class QuizForm extends StatefulWidget {
const QuizForm({Key? key}) : super(key: key);
#override
State<QuizForm> createState() => _QuizFormState();
}
class _QuizFormState extends State<QuizForm> {
Question _cachedQuestion;
bool _submitEnabled = false;
Future<Question> getQuestion() async {
if (_cachedQuestion != null) {
return _cachedQuestion;
}
final response = await http.get('https://your-api-endpoint.com/question');
if (response.statusCode == 200) {
final question = Question.fromJson(json.decode(response.body));
_cachedQuestion = question;
_submitEnabled = true;
return question;
} else {
throw Exception('Failed to fetch question');
}
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: FutureBuilder(
future: getQuestion(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final question = snapshot.data;
return ListView(
children: [
ListTile(title: Text(question.title)),
ListTile(title: Text(
I managed to get it through ValueListenableBuilder. Here is my code that is working as expected:
import 'package:flutter/material.dart';
class QuizForm extends StatefulWidget {
const QuizForm({Key? key}) : super(key: key);
#override
State<QuizForm> createState() => _QuizFormState();
}
class _QuizFormState extends State<QuizForm> {
final _buttonEnabled = ValueNotifier(true);
int buildCount = 0;
getQuestion () {}
#override
Widget build(BuildContext context) {
print(buildCount);
return SafeArea(
child: Scaffold(
body: FutureBuilder(
future: getQuestion(),
builder: (context, snapshot) {
return ListView(
children: [
ListTile(title: Text('Quiz Title')),
ListTile(title: Text('1')),
ListTile(title: Text('2')),
ListTile(title: Text('3')),
ListTile(title: Text('4')),
SizedBox(height: 20),
ValueListenableBuilder(
valueListenable: _buttonEnabled,
builder: (context, value, child) {
return ElevatedButton(
onPressed: _buttonEnabled.value
? () async {
_buttonEnabled.value = false;
print('Please Wait, Answer is getting Saved');
await Future.delayed(const Duration(seconds: 3));
_buttonEnabled.value = true;
buildCount++;
setState(() {
});
}
: null,
child: child,
);
},
child: Text('Submit Quiz'),
),
],
);
},
),
),
);
}
}

Flutter Listview.Builder inside bottom sheet widget not loading data on load

The below code does not display any data when the bottomsheet loads. Once the bottomsheet is loaded if I do a save operation on the code editor it loads the data. What am I missing here?
I have a bottomsheet widget which is invoked using a button.
_showBottomSheet() {
showModalBottomSheet(
context: context,
builder: (context) {
return const Contacts();
},
);
}
The above code loads up the Contacts widget that has a Listview.builder in it which is below.
class Contacts extends StatefulWidget {
const Contacts({Key? key}) : super(key: key);
#override
_ContactsState createState() => _ContactsState();
}
class _ContactsState extends State<Contacts> {
List<PhoneBookContact> phoneBookContacts1 = [];
List<PhoneBookContact> phoneBookContacts2 = [];
#override
void initState() {
loadContacts();
super.initState();
}
Future loadContacts() async {
///somecode to gather data for the listview builder
///populates the phoneBookContacts1 & phoneBookContacts2 lists
}
#override
Widget build(BuildContext context) {
return Column(children: [
const Text('Contacts Set 1'),
displayPhoneBookContacts(phoneBookContacts1),
const Text('Contacts Set 2'),
displayPhoneBookContacts(phoneBookContacts2),
]);
}
Widget displayPhoneBookContacts(phoneBookContacts) {
return Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: phoneBookContacts.length,
itemBuilder: (BuildContext context, int index) {
return Card(
margin: const EdgeInsets.all(10),
child: ListTile(
contentPadding: const EdgeInsets.all(10),
title: Column(
children: [
Text(phoneBookContacts[index].phoneBookContact.toString()),
const SizedBox(
height: 20,
),
ListView.separated(
physics: const ClampingScrollPhysics(),
shrinkWrap: true,
itemCount: phoneBookContacts[index].contactNumbers!.length,
separatorBuilder: (BuildContext context, int index) =>
const Divider(),
itemBuilder: (BuildContext context, int phoneIndex) {
return InkWell(
onTap: () {},
child: Row(
children: [
Text(phoneBookContacts[index]
.contactNumbers![phoneIndex]
.phone),
],
),
);
},
),
],
),
),
);
},
),
);
}
}
I don't prefer using FutureBuilder inside StatefulWidget., it will recall the API(future) on every setState. As for comment it is missing setState after initializing the data.
#override
void initState() {
super.initState();
loadContacts();
}
Future loadContacts() async {
///somecode to gather data for the listview builder
///populates the phoneBookContacts1 & phoneBookContacts2
if(mounted){
// if widget build then setState call.if not we don't need to call setState
// for every initstate data loading, we have to ensure it if widget is build or not. most of the case user close screen when data loading, then error happens
setState(() {});// make sure to call setState
}
}
Because function initState() don't await your loadContacts(), data loaded after function build().
You need use FutureBuilder class to rebuild ListView widget after load data
Example:
FutureBuilder(
future: loadContacts(),
builder:(context, AsyncSnapshot snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
return Container(
child: ListView.builder(
itemCount: _faouriteList.length,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
return Text('${_faouriteList[index].title}');
}
)
);
}
}
)

How to get value from an object which in the state (flutter_bloc)

in builder method I reach the value of state like
return BlocBuilder<UsersOwnProfileBloc, UsersOwnProfileState>(
cubit: widget.bloc,
builder: (context, state) {
if (state is FetchedUserSettingState) {
bool account = state.userSettings.publicAccount
}
But I need to get the values from initState. I need to set the values of the widget. I tried something like this but I got error
#override
void initState() {
super.initState();
UsersOwnProfileState state = BlocProvider.of<UsersOwnProfileBloc>(context).state;
if (state is FetchedUserSettingState) {
publicAccount = state.userSettings.publicAccount;
}
}
Can anyone show me how to get state value in initState?
class UserSettingPage extends StatefulWidget {
final UsersOwnProfileBloc bloc;
const UserSettingPage({Key key, this.bloc}) : super(key: key);
#override
_UserSettingPageState createState() => _UserSettingPageState();
}
class _UserSettingPageState extends State<UserSettingPage> {
bool newListingAlert;
bool listingForSearchAlert;
bool searchForListingAlert;
bool followAlert;
bool publicAccount;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final state = BlocProvider.of<UsersOwnProfileBloc>(context).state;
if (state is FetchedUserSettingState) {
publicAccount = state.userSettings.publicAccount;
}
});
}
#override
Widget build(BuildContext context) {
return BlocBuilder<UsersOwnProfileBloc, UsersOwnProfileState>(
cubit: widget.bloc,
builder: (context, state) {
if (state is FetchedUserSettingState) {
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(25.h),
child: ListingEditAppBar(
onCancel: () {
widget.bloc.add(FetchUserEvent(userId: CurrentUser.currentUser.id));
Navigator.pop(context);
},
),
),
body: Column(
children: [
PageTitle(title: "Preferences"),
Expanded(
child: ListView(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("Profilim herkese açık"),
Switch(
onChanged: (value) {
setState(() {
publicAccount = value;
});
},
value: publicAccount,
)
],
),
)
],
),
)
],
),
);
}
return MyProgressIndicator();
},
);
}
}
I have added the whole code. I am getting the following error.
Failed assertion: boolean expression must not be null
The relevant error-causing widget was
Switch
If you would like to access the state within initState you will need to use WidgetsBinding to access this. However, using this ensures that your widget is built and then triggers the method to get the value. It will be faster to just use the BlocBuilder, Watch, or Select to get the value you are looking for.
But to answer your question, you can do the following
WidgetsBinding.instance.addPostFrameCallback((_) {
final state = BlocProvider.of<UsersOwnProfileBloc>(context).state;
if (state is FetchedUserSettingState) {
publicAccount = state.userSettings.publicAccount;
}
});

Looking up a deactivated widget's ancestor is unsafe. How do i Save a reference?

To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.
How do i save a reference, i don't have any idea, i am new to flutter.
This is my sample code:Images are in order
First
second
third
class QuestionFeed extends StatefulWidget {
final FirebaseUser loggedInUser;
QuestionFeed({ this.loggedInUser });
#override
_QuestionFeedState createState() => _QuestionFeedState();
}
class _QuestionFeedState extends State<QuestionFeed> {
List<Widget> questions;
List<String> followingsList=[];
bool x;
final _scaffoldKey=GlobalKey<ScaffoldState>();
retrieveTimeLine()async{
QuerySnapshot querySnapshot =await
questionsFeedReference.orderBy("timeStamp",descending:
true).getDocuments();
setState(() {
List<Widget> allQuestions=querySnapshot.documents.where((element) {
if(element["type"]=="question" ){
x=true;
return true;
}
else if(element["type"]=="answer"){
x=false;
return true;
}
else{
return false;
}
}).map((document)=>x?Question.fromDocument(document):
StreamBuilder<DocumentSnapshot>(
stream: answersReference.document("038281b3-3842-4ea9-b4d8-
3155a571826e").snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
backgroundColor: Colors.lightBlueAccent,
),
);
}
NotusDocument _document;
ZefyrController _zefyrController;
FocusNode _focusNode;
final messages = snapshot.data;
final messageText = messages.data['answer'];
_document =NotusDocument.fromJson( jsonDecode(messageText));
_zefyrController = ZefyrController(_document);
_focusNode=FocusNode();
return Container(
height: 400,
child: ZefyrScaffold(
child: ZefyrEditor(
padding: EdgeInsets.all(16),
controller: _zefyrController,
mode: ZefyrMode.view,
focusNode: _focusNode,
imageDelegate: MyAppZefyrImageDelegate(),
physics: NeverScrollableScrollPhysics(),
),
),
);
},
)
).toList();
this.questions=allQuestions;
});
}
#override
void initState() {
// TODO: implement initState
super.initState();
retrieveTimeLine();
}
createUserTimeLine(){
retrieveTimeLine();
if(questions==null){
return circularProgress();
}
else{
return ListView(children: questions,);
}
}
#override
Widget build(context) {
return Scaffold(
key: _scaffoldKey,
appBar: header(
context,
isAppTitle: true,
),
body: RefreshIndicator(
child: createUserTimeLine(),onRefresh()=>retrieveTimeLine(),),
);
}
}
I am using Zefyr Editor and then retriving its data and showing it in Container

Flutter StreamController returning duplicate data

I have my widgets setup in the following hierarchy to pass data between the two tabs
DataShareWidget
TabBarView
InputManagment
InfiniteListView
The DataShareWidget extends InheritedWidget and contains the ShareData class which has a StreamController to send and receive data.
but on the receive side (InfiniteListView tab) am getting duplicate data.
I've printing out the raw data from InputManagment before entering the stream, but there does not appear to be any duplicate data, so it must be something with the stream.
Here the relevant code from the main file
class ShareData {
final StreamController _streamController = StreamController.broadcast();
Stream get stream => _streamController.stream;
Sink get sink => _streamController.sink;
}
class DataShareWidget extends InheritedWidget {
final ShareData data;
DataShareWidget({
Key key,
#required Widget child,
}) :assert(child != null),
data = ShareData(),
super(key: key, child: child);
static ShareData of (BuildContext context) => (context.inheritFromWidgetOfExactType(DataShareWidget) as DataShareWidget).data;
#override
bool updateShouldNotify(DataShareWidget old) => false;
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("MyApp"),
bottom: TabBar(
tabs: Tabs,
controller: _tabController,
),
),
body: DataShareWidget(
child: TabBarView(
controller: _tabController,
children: [
InputManagment(),
InfiniteListView(),
],
),
),
);
}
In the data management file i have this line of code to add data
DataShareWidget.of(context).sink.add(inputData);
And here is the code for InfiniteListView
class _InfiniteScrollListViewState extends State<InfiniteScrollListView> with AutomaticKeepAliveClientMixin<InfiniteScrollListView>{
#override
bool get wantKeepAlive => true;
ScrollController _scrollController = ScrollController();
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
var _listViewData = new List();
_dataFormat(data){
var time = DateFormat('kk:mm:ss').format(DateTime.now());
var timeStampedData = time.toString() + "| " + data;
setState(() {_listViewData.add(timeStampedData); });
}
#override
Widget build(BuildContext context) {
DataShareWidget.of(context).stream.listen((data) => _dataFormat(data));
return ListView.builder(
itemCount: _listViewData.length,
controller: _scrollController,
itemBuilder: (context, index) {
return ListTile(
title: AutoSizeText(_listViewData[index], maxLines: 2),
dense: true,
);
},
);
}
}
EDIT: As per #jamesdlin suggestion i've refactored the code using StreamBuilder, and that appears to have solved the issue, here is the updated code below.
_dataFormat(data){
var time = DateFormat('kk:mm:ss').format(DateTime.now());
var timeStampedData = time.toString() + "| " + data;
_listViewData.add(timeStampedData);
}
#override
Widget build(BuildContext context) {
//_scrollToBottom();
return StreamBuilder(
stream: DataShareWidget.of(context).stream,
builder: (BuildContext context, AsyncSnapshot snapshot){
if(snapshot.hasError){ return Text(snapshot.error);}
if(snapshot.hasData){
_dataFormat(snapshot.data);
return ListView.builder(
itemCount: _listViewData.length,
controller: _scrollController,
itemBuilder: (context, index) {
return ListTile(
title: AutoSizeText(_listViewData[index], maxLines: 2),
dense: true,
);
},
);
}
}
);
You call listen on the Stream every time _InfiniteScrollListViewState.build is called. That will result in your callback being invoked multiple times. You should listen to the Stream only once.
You also perhaps should consider using a StreamBuilder widget instead.