how to use useStreamController in HookWidget? - flutter

Im new to flutter hooks, and riverpod(state-manangement),
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _url = "https://owlbot.info/api/v4/dictionary/";
String _token = "ae7cbdfff57e548a4360348ee519123a741d8e3d";
TextEditingController _controller = TextEditingController();
StreamController _streamController;
Stream _stream;
Timer _debounce;
Future _search() async {
if (_controller.text == null || _controller.text.length == 0) {
_streamController.add(null);
return;
}
_streamController.add("waiting");
Response response = await get(_url + _controller.text.trim(),
headers: {"Authorization": "Token " + _token});
_streamController.add(json.decode(response.body));
}
#override
void initState() {
super.initState();
_streamController = StreamController();
_stream = _streamController.stream;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Flictionary"),
bottom: PreferredSize(
preferredSize: Size.fromHeight(48.0),
child: Row(
children: <Widget>[
Expanded(
child: Container(
margin: const EdgeInsets.only(left: 12.0, bottom: 8.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.0),
),
child: TextFormField(
onChanged: (String text) {
if (_debounce?.isActive ?? false) _debounce.cancel();
_debounce = Timer(const Duration(milliseconds: 1000), () {
_search();
});
},
controller: _controller,
decoration: InputDecoration(
hintText: "Search for a word",
contentPadding: const EdgeInsets.only(left: 24.0),
border: InputBorder.none,
),
),
),
),
IconButton(
icon: Icon(
Icons.search,
color: Colors.white,
),
onPressed: () {
_search();
},
)
],
),
),
),
body: Container(
margin: const EdgeInsets.all(8.0),
child: StreamBuilder(
stream: _stream,
builder: (BuildContext ctx, AsyncSnapshot snapshot) {
if (snapshot.data == null) {
return Center(
child: Text("Enter a search word"),
);
}
if (snapshot.data == "waiting") {
return Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data["definitions"].length,
itemBuilder: (BuildContext context, int index) {
return ListBody(
children: <Widget>[
Container(
color: Colors.grey[300],
child: ListTile(
leading: snapshot.data["definitions"][index]
["image_url"] ==
null
? null
: CircleAvatar(
backgroundImage: NetworkImage(snapshot
.data["definitions"][index]["image_url"]),
),
title: Text(_controller.text.trim() +
"(" +
snapshot.data["definitions"][index]["type"] +
")"),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
snapshot.data["definitions"][index]["definition"]),
)
],
);
},
);
},
),
),
);
}
}
i just wanted to convert above statefulWidget to HookWidget and how to use riverpod as statemanagenent for the above example. I know some basics of hooks and riverpod, but still i'm confused between the hooks, statemanagement(riverpod).
Please Can someone help understand them and provide some examples or at least convert the above code into the hook widget, and using hookbuilder
Thanks in advance

First off, the code:
final textProvider = StateProvider<String>((_) => '');
final responseFutureProvider =
FutureProvider.autoDispose.family<Response, String>((ref, text) async {
if (text == null || text.length == 0) {
throw Error();
}
final String _url = "https://owlbot.info/api/v4/dictionary/";
final String _token = "ae7cbdfff57e548a4360348ee519123a741d8e3d";
return await get(_url + text.trim(), headers: {"Authorization": "Token " + _token});
});
final responseProvider = Computed<AsyncValue<Response>>((read) {
final text = read(textProvider).state;
return read(responseFutureProvider(text));
});
String _useDebouncedSearch(TextEditingController controller) {
final search = useState(controller.text);
useEffect(() {
Timer? timer;
void listener() {
timer?.cancel();
timer = Timer(
const Duration(milliseconds: 1000),
() => search.value = controller.text,
);
}
controller.addListener(listener);
return () {
timer?.cancel();
controller.removeListener(listener);
};
}, [controller]);
return search.value;
}
class MyHomePage extends HookWidget {
const MyHomePage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final controller = useTextEditingController();
final text = useProvider(textProvider);
text.state = _useDebouncedSearch(controller);
return Scaffold(
appBar: AppBar(
title: Text("Flictionary"),
bottom: PreferredSize(
preferredSize: Size.fromHeight(48.0),
child: Row(
children: <Widget>[
Expanded(
child: Container(
margin: const EdgeInsets.only(left: 12.0, bottom: 8.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.0),
),
child: TextFormField(
controller: controller,
decoration: InputDecoration(
hintText: "Search for a word",
contentPadding: const EdgeInsets.only(left: 24.0),
border: InputBorder.none,
),
),
),
),
Icon(
Icons.search,
color: Colors.white,
),
],
),
),
),
body: MyHomePageBody(),
);
}
}
class MyHomePageBody extends HookWidget {
const MyHomePageBody({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final text = useProvider(textProvider).state;
final response = useProvider(responseProvider);
response.when(
error: (err, stack) => Center(child: Text('Error: $err')),
loading: () => Center(child: CircularProgressIndicator()),
data: (response) => ListView.builder(
itemCount: Response["definitions"].length,
itemBuilder: (BuildContext context, int index) {
return ListBody(
children: <Widget>[
Container(
color: Colors.grey[300],
child: ListTile(
leading: response["definitions"][index]["image_url"] == null
? null
: CircleAvatar(
backgroundImage:
NetworkImage(response["definitions"][index]["image_url"]),
),
title: Text(text.trim() + "(" + response["definitions"][index]["type"] + ")"),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(response["definitions"][index]["definition"]),
)
],
);
},
),
);
}
}
We add an external text provider so that we can read the text field from other providers.
We create a FutureProviderFamily so that we can perform the API call with a parameter, the text from your text field. In Riverpod, families enable the passing parameters to providers.
We create a Computed that will call the Future every time the value of the text provider changes. This returns an AsyncValue which is a wonderful replacement to the StreamBuilder you were using (will explain more).
Refactored your debounced search a bit to use the useEffect hook. This will handle disposing the resources for your timer and update the textProvider as necessary. (I learned this from Remi's Marvel example)
We no longer need an onChanged or manual button press to search, as the textprovider's state is being updated whenever the controller changes.
Moved the body of your page into its own class to separate what needs to be loaded from what is static.
Now, instead of a StreamBuilder, we can use AsyncValue to handle the loading, error, and success states of your build.
I know this was a lot to cover, so I'd recommend really digging into the docs to learn more about everything that's in this example.

Related

How to return the PageView to its initial state in Flutter

I am making a quiz app and at first everything works fine, but when I do a quiz the first time, it does the correct or incorrect answer check perfectly.
But when I go back to quiz without restarting the app just navigating from one page to another the PageView does not reset its state again.
Before taking the quiz
enter image description here
After I do the quiz and I want to do it again without restart the app, I get the checked answers.
enter image description here
How to return the PageView to its initial state without restart the app
Here is my code:
import 'package:flutter/material.dart';
import 'package:quizapp/src/models/quiz_model.dart';
import 'package:quizapp/src/screens/result_screen.dart';
class QuizScreen extends StatefulWidget {
const QuizScreen({Key? key}) : super(key: key);
#override
State<QuizScreen> createState() => _QuizScreenState();
}
class _QuizScreenState extends State<QuizScreen> {
int _questionNumber = 1;
late PageController _controller;
int _score = 0;
#override
void initState() {
_controller = PageController(initialPage: 0);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: PageView.builder(
physics: const NeverScrollableScrollPhysics(),
controller: _controller,
itemCount: questions.length,
itemBuilder: (context, index) {
final _question = questions[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 16,
),
Text(
_question.text,
style: const TextStyle(fontSize: 22),
),
const SizedBox(
height: 16,
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: _question.options
.map((option) => GestureDetector(
onTap: () {
Future.delayed(
const Duration(milliseconds: 250),
() {
if (_questionNumber <
questions.length) {
_controller.nextPage(
duration: const Duration(
milliseconds: 250),
curve: Curves.easeInExpo);
setState(() {
if (option.isCorrect == true) {
_score++;
}
});
setState(() {
_questionNumber++;
// _isLocked = false;
});
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) =>
ResultScreen(
score: _score),
));
}
});
if (_question.isLocked) {
return;
} else {
setState(() {
_question.isLocked = true;
_question.selectedOption = option;
});
}
},
child: Container(
height: 50,
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.symmetric(
vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF6949FD),
borderRadius:
BorderRadius.circular(16),
border: Border.all(
color: getColorForOption(
option, _question))),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
option.text,
style: const TextStyle(
fontSize: 18,
color: Colors.white),
),
const SizedBox(width: 10),
getIconForOption(option, _question)
],
),
),
))
.toList(),
)))
]);
},
)),
),
],
),
));
}
Color getColorForOption(Option option, Question _question) {
final isSelected = option == _question.selectedOption;
if (_question.isLocked) {
if (isSelected) {
return option.isCorrect ? Colors.green : Colors.red;
} else if (option.isCorrect) {
return Colors.green;
}
}
return const Color(0xFF6949FD);
}
Widget getIconForOption(Option option, Question _question) {
final isSelected = option == _question.selectedOption;
if (_question.isLocked) {
if (isSelected) {
return option.isCorrect
? const Icon(Icons.check_circle, color: Colors.green)
: const Icon(Icons.cancel, color: Colors.red);
} else if (option.isCorrect) {
return const Icon(Icons.check_circle, color: Colors.green);
}
}
return const SizedBox.shrink();
}
}
An easier way is to restart the app when you go back or press a button. You can wrap Scaffold() with WillPopScope() to restart when you back. You can use this package to restart.
If you need to save the score, you can save it in local storage. Another easy package for this is get_storage.
dependencies:
flutter_phoenix: ^1.1.0
runApp(Phoenix(child: const MyApp()));
WillPopScope(
onWillPop: () async {
Phoenix.rebirth(context);
},
child: Scaffold())

How to send a data from listview screen to form screen using flutter

I am trying to send a data from ontap listview screen to form screen like image below. I have searched many references on google but I can't find any references that can help me, if you can provide solutions or references, I will greatly appreciate it.
enter image description here
This is my sample code (ListPage Screen) :
const ListPage({Key? key}) : super(key: key);
#override
State<ListPage> createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
TextEditingController textFieldController = TextEditingController();
var _controller = TextEditingController();
late bool searching, error;
var data;
late String query;
String dataurl = "https://www.something.co.id/mobile/search_data.php";
#override
void initState() {
searching = true;
error = false;
query = "";
super.initState();
}
void getSuggestion() async {
//get suggestion function
var res = await http
.post((Uri.parse(dataurl + "?query=" + Uri.encodeComponent(query))));
//in query there might be unwant character so, we encode the query to url
if (res.statusCode == 200) {
setState(() {
data = json.decode(res.body);
//update data value and UI
});
} else {
//there is error
setState(() {
error = true;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
toolbarHeight: AppLayout.getHeight(100),
automaticallyImplyLeading: false,
title: searchField(),
backgroundColor: Styles.background,
elevation: 0.0,
),
body: SingleChildScrollView(
child: Container(
alignment: Alignment.center,
child: data == null
? Container(
padding: EdgeInsets.all(20),
child: searching
? Text("Please wait")
: Text("Search any location")
//if is searching then show "Please wait"
//else show search peopels text
)
: Container(
child: searching
? showSearchSuggestions()
: Text("Find any location"),
)
// if data is null or not retrived then
// show message, else show suggestion
),
),
);
}
Widget showSearchSuggestions() {
List suggestionlist = List.from(data["data"].map((i) {
return SearchSuggestion.fromJSON(i);
}));
//serilizing json data inside model list.
return Column(
children: suggestionlist.map((suggestion) {
return InkResponse(
// onTap: () {
// //when tapped on suggestion
// print(suggestion.id); //pint student id
// },
child: GestureDetector(
onTap: () {
_sendDataBack(context);
},
child: SizedBox(
width: double.infinity, //make 100% width
child: Card(
child: Container(
decoration: BoxDecoration(color: Styles.background),
padding: EdgeInsets.all(15),
child: Text(
suggestion.name,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
);
}).toList(),
);
}
// get the text in the TextField and send it back to the FirstScreen
void _sendDataBack(BuildContext context) {
String textToSendBack = textFieldController.text;
Navigator.pop(context, textToSendBack);
}
Widget searchField() {
//search input field
return Container(
height: 50,
child: TextField(
controller: _controller,
autofocus: true,
style: Styles.textStyle,
decoration: InputDecoration(
hintStyle: TextStyle(color: Styles.colorDeepGrey),
hintText: "Search Location...",
prefixIcon: Icon(Icons.search),
suffixIcon: _controller.text.length > 0
? IconButton(
onPressed: () {
_controller.clear();
setState(() {});
},
icon: Icon(Icons.cancel, color: Colors.grey))
: null,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Styles.colorLightBlack.withOpacity(0.20),
width: 2,
),
borderRadius: BorderRadius.circular(4),
), //under line border, set OutlineInputBorder() for all side border
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Styles.primaryColor,
width: 1,
),
borderRadius: BorderRadius.circular(4),
), // focused border color
), //decoration for search input field
onChanged: (value) {
query = value; //update the value of query
getSuggestion(); //start to get suggestion
},
),
);
}
}
//serarch suggestion data model to serialize JSON data
class SearchSuggestion {
String id, name;
SearchSuggestion({required this.id, required this.name});
factory SearchSuggestion.fromJSON(Map<String, dynamic> json) {
return SearchSuggestion(
id: json["id"],
name: json["name"],
);
}
}
Sample Code NextPage Screen :
class NextPage extends StatefulWidget {
#override
_NextPageState createState() => _NextPageState();
}
class _NextPageState extends State<NextPage> {
int _currentStep = 0;
StepperType stepperType = StepperType.vertical;
String text = 'Text';
var _controller = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text('Flutter Stepper Demo'),
centerTitle: true,
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Stepper(
type: stepperType,
physics: ScrollPhysics(),
currentStep: _currentStep,
onStepTapped: (step) => tapped(step),
onStepContinue: continued,
onStepCancel: cancel,
steps: <Step>[
//Form Pengirim
Step(
title: new Text('Location'),
content: Column(
children: <Widget>[
SizedBox(
height: 50,
child: TextField(
onTap: () {
_awaitReturnValueFromSecondScreen(context);
},
controller: _controller,
autofocus: true,
onChanged: (text) {
setState(() {});
},
style: Styles.textStyle,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
hintText: 'Location',
contentPadding:
EdgeInsets.only(left: 15, right: 15),
hintStyle: TextStyle(color: Styles.colorDeepGrey),
suffixIcon: _controller.text.length > 0
? IconButton(
onPressed: () {
_controller.clear();
setState(() {});
},
icon: Icon(Icons.cancel,
color: Colors.grey))
: null,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color:
Styles.colorLightBlack.withOpacity(0.20),
width: 2,
),
borderRadius: BorderRadius.circular(4),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Styles.primaryColor,
width: 1,
),
borderRadius: BorderRadius.circular(4),
),
),
),
),
],
),
isActive: _currentStep >= 0,
state: _currentStep >= 0
? StepState.complete
: StepState.disabled,
),
],
),
),
],
),
),
);
}
void _awaitReturnValueFromSecondScreen(BuildContext context) async {
// start the SecondScreen and wait for it to finish with a result
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DataAlamat(),
));
// after the SecondScreen result comes back update the Text widget with it
setState(() {
text = result;
});
}
tapped(int step) {
setState(() => _currentStep = step);
}
continued() {
_currentStep < 2 ? setState(() => _currentStep += 1) : null;
}
cancel() {
_currentStep > 0 ? setState(() => _currentStep -= 1) : null;
}
}
Pass the tapped item value to the next page via named parameter of other page class.
class ListPage extends StatelessWidget {
const ListPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return ListTile(
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return NextPage(value: index);
},
));
},
title: Text(index.toString()),
);
},
),
);
}
}
class NextPage extends StatelessWidget {
final int value;
const NextPage({Key? key, required this.value}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(value.toString()),
),
);
}
}
Example in ListView screen, you have a variable called List<String> listLocations. Your ListView widget be like:
ListView.builder(
itemCount: listLocations.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) {
return SecondScreen(listLocations[index]);
},
)),
child: ...
);
}
}
And your SecondScreen is a StatefulWidget (well it is a Form screen, so it would be Stateful, not Stateless, use TextEditingController in Form widget):
import 'package:flutter/material.dart';
class SecondScreen extends StatefulWidget {
final String location;
SecondScreen(this.location, {Key? key}) : super(key: key);
#override
State<SecondScreen> createState() => _SecondScreenState();
}
class _SecondScreenState extends State<SecondScreen> {
var _textEditingController = TextEditingController();
#override
void initState() {
_textEditingController.text = widget.location;
super.initState();
}
#override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
You need to pass the location value in init state, and don't forget to dispose it.

Search Result does not update instantly flutter

I'm emulating this search and filter github here and the codes are almost the same but the filtered results do not update instantly while I type and also I faced the following issues:
I will have to press enter on my laptop to finally get the filtered list
When I hit the close icon(which is to clear all the words), I will have to tap the searchbar again so that all my listtile are back on the listview.
Here's my code:
class _CurrencySelectState extends State<CurrencySelect> {
late List<Currency> resCur;
String query = '';
#override
void initState() {
super.initState();
resCur = currencyList;
}
void searchCur(String query) {
final List<Currency> filteredCur = currencyList.where((cur) {
final symbolLower = cur.symbol.toLowerCase(); // Search using symbol
final nameLower = cur.country.toLowerCase(); // Search using country
final searchLower = query.toLowerCase();
return symbolLower.contains(searchLower) ||
nameLower.contains(searchLower);
}).toList();
setState(() {
this.query = query;
resCur = filteredCur;
});
}
#override
Widget build(BuildContext context) {
Widget buildCur(Currency cur) => ListTile(
leading: Padding(
padding: EdgeInset.all(5)
child: SizedBox(
child: Column(
children: <Widget>[
SvgPicture.asset(
cur.assetPath,
),
]),
),
),
title: Column(
children: [
Text(
cur.symbol,
style: TextStyle(
...
),
Text(
cur.name,
style: TextStyle(
...
),
],
),
trailing: Text(
"0.25",
style: TextStyle(
...
),
);
return TextButton(
onPressed: () async {
showModalBottomSheet(
enableDrag: false,
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return DraggableScrollableSheet(
expand: false,
builder: (context, scrollController) {
return Column(
children: <Widget>[
SearchWidget(
text: query,
onChanged: searchCur,
hintText: "Enter symbol or country"
),
Expanded(
child: ListView.builder(
controller: scrollController,
itemCount: resCur.length,
itemBuilder: (context, int index) {
final cur = resCur[index];
return buildCur(cur);
},
),
)
],
);
},
);
});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
...
),
SvgPicture.asset(
...
)
],
));
}
}
Searchwidget code:
import 'package:flutter/material.dart';
class SearchWidget extends StatefulWidget {
final String text;
final ValueChanged<String> onChanged;
final String hintText;
const SearchWidget({
Key? key,
required this.text,
required this.onChanged,
required this.hintText,
}) : super(key: key);
#override
_SearchWidgetState createState() => _SearchWidgetState();
}
class _SearchWidgetState extends State<SearchWidget> {
final controller = TextEditingController();
#override
Widget build(BuildContext context) {
final styleActive = TextStyle(color: Colors.black);
final styleHint = TextStyle(color: Colors.black54);
final style = widget.text.isEmpty ? styleHint : styleActive;
return Container(
height: 42,
margin: const EdgeInsets.fromLTRB(16, 16, 16, 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
border: Border.all(color: Colors.black26),
),
padding: const EdgeInsets.symmetric(horizontal: 8),
child: TextField(
controller: controller,
decoration: InputDecoration(
icon: Icon(Icons.search, color: style.color),
suffixIcon: widget.text.isNotEmpty
? GestureDetector(
child: Icon(Icons.close, color: style.color),
onTap: () {
controller.clear();
widget.onChanged('');
FocusScope.of(context).requestFocus(FocusNode());
},
)
: null,
hintText: widget.hintText,
hintStyle: style,
border: InputBorder.none,
),
style: style,
onChanged: widget.onChanged,
),
);
}
}

flutter riverpod leaving screen then come back it doesn't maintain the state

So I have two screens:
-Book_screen to display all the books(click on any book to go to article_screen)
-article_screen to display articles
In article_screen, I can click on article to save it as favorites.
but when I go back to book_screen then come back to article_screen, those favorited articles doesn't show the favorited status(icon red heart).
this is my article screen code:
class ArticleENPage extends ConsumerStatefulWidget{
final String bookName;
const ArticleENPage({Key? key,#PathParam() required this.bookName,}) : super(key: key);
#override
ArticleENScreen createState()=> ArticleENScreen();
}
class ArticleENScreen extends ConsumerState<ArticleENPage> {
late Future<List<Code>> codes;
#override
void initState() {
super.initState();
codes = fetchCodes();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.bookName,style: const TextStyle(fontSize: 24,fontWeight: FontWeight.bold),),backgroundColor: Colors.white,foregroundColor: Colors.black,elevation: 0,),
body: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: Container(
margin: const EdgeInsets.only(top:10),
height: 43,
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50),
border: Border.all(
color: Colors.black.withOpacity(0.32),
),
),
child: Consumer(
builder: (context,ref,_) {
return TextField(
onChanged: (value) {
searchStringController controller = ref.read(searchStringProvider.notifier);
controller.setText(value.toLowerCase());
},
decoration: const InputDecoration(
border: InputBorder.none,
icon: Icon(Icons.search,size:18),
hintText: "Search Here",
hintStyle: TextStyle(color: Color.fromRGBO(128,128, 128, 1)),
),
);
}
),
),
),
const SizedBox(height: 10),
Expanded(
child: FutureBuilder(
builder: (context, AsyncSnapshot<List<Code>> snapshot) {
if (snapshot.hasData) {
return Center(
child: Consumer(
builder: (context,ref,child) {
final searchString = ref.watch(searchStringProvider);
return ListView.separated(
padding: const EdgeInsets.all(8),
itemCount: snapshot.data!.length,
itemBuilder: (BuildContext context, int index) {
return snapshot.data![index].name
.toLowerCase()
.contains(searchString) ||
snapshot.data![index].description
.toLowerCase()
.contains(searchString)
? Consumer(
builder: (context,ref,child) {
final favlist = ref.watch(FavoriteListController.favoriteListProvider);
print(favlist);
final alreadySaved = favlist.contains(snapshot.data![index]);
return Card(
child:Padding(
padding: const EdgeInsets.all(10),
child:ExpandableNotifier(
child: ScrollOnExpand(
child: ExpandablePanel(
theme: const ExpandableThemeData(hasIcon: true),
header: RichText(text: TextSpan(children: highlight(snapshot.data![index].name, searchString,'title')),),
collapsed: RichText(text: TextSpan(children: highlight(snapshot.data![index].description, searchString,'content')), softWrap: true, maxLines: 3, overflow: TextOverflow.ellipsis,),
expanded: Column(
children: [
RichText(text: TextSpan(children: highlight(snapshot.data![index].description, searchString,'content')), softWrap: true ),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
semanticLabel: alreadySaved ? 'Remove from saved' : 'Save',
),
onPressed: () {
FavoriteListController controller = ref.read(FavoriteListController.favoriteListProvider.notifier);
if (alreadySaved) {
controller.toggle(snapshot.data![index]);
} else {
controller.toggle(snapshot.data![index]);
}
},
),
IconButton(
icon: const Icon(Icons.content_copy),
onPressed: () {
setState(() {
Clipboard.setData(ClipboardData(text: snapshot.data![index].name+"\n"+snapshot.data![index].description))
.then((value) {
ScaffoldMessenger.of(context).showSnackBar(new SnackBar(content: Text('Copied')));
},);
});
},
),],),],)),),)));})
: Container();
},
separatorBuilder: (BuildContext context, int index) {
return snapshot.data![index].name
.toLowerCase()
.contains(searchString) ||
snapshot.data![index].description
.toLowerCase()
.contains(searchString)
? Divider()
: Container();
},
);
}
),
);
} else if (snapshot.hasError) {
return const Center(child: Text('Something went wrong :('));
}
return const Align(alignment:Alignment.topCenter,child:CircularProgressIndicator());
},
future: codes,
),
),
],
),
);
}
//read from files
Future<List<Code>> fetchCodes() async {
final response =
await rootBundle.loadString('assets/articles.json');
var CodeJson = json.decode(response)[widget.bookName] as List<dynamic>;
return CodeJson.map((code) => Code.fromJson(code)).toList();
}
}
I tried using riverpod for provider and save to sharedpreference the list of code that I favorited.
final sharedPrefs =
FutureProvider<SharedPreferences>((_) async => await SharedPreferences.getInstance());
class FavoriteListController extends StateNotifier<List<Code>>{
FavoriteListController(this.pref) : super(Code.decode(pref?.getString("favcode")??""));
static final favoriteListProvider = StateNotifierProvider<FavoriteListController, List<Code>>((ref) {
final pref = ref.watch(sharedPrefs).maybeWhen(
data: (value) => value,
orElse: () => null,
);
print(pref?.getString("favcode"));
return FavoriteListController(pref);
});
final SharedPreferences? pref;
void toggle(Code code) {
if (state.contains(code)) {
state = state.where((id) => id != code).toList();
} else {
state = [...state, code];
}
final String encodedData = Code.encode(state);
pref!.setString("favcode", encodedData);
}
}
I am not sure what is the cause of this but I think it might be because of futurebuilder? I am confused to how to solve this issue...
I am stuck in a dead end so any help or advice would be really appreciated
edit 1-
this is my source code in case I have not include all the necessary codes
https://github.com/sopheareachte/LawCode
edit-2
do I need to change "late Future<List> codes;" that fetch all the codes for futurebuilder to riverpod futureprovider too for it to work?
Maybe the problem is, that you define a static provider inside of your controller class. Try this code:
final sharedPrefs = FutureProvider<SharedPreferences>((_) async => await SharedPreferences.getInstance());
final favoriteListProvider = StateNotifierProvider<FavoriteListController, List<Code>>((ref) {
final pref = ref.watch(sharedPrefs).maybeWhen(
data: (value) => value,
orElse: () => null,
);
print(pref?.getString("favcode"));
return FavoriteListController(pref);
});
class FavoriteListController extends StateNotifier<List<Code>>{
FavoriteListController(this.pref) : super(Code.decode(pref?.getString("favcode")??""));
final SharedPreferences? pref;
void toggle(Code code) {
if (state.contains(code)) {
state = state.where((id) => id != code).toList();
} else {
state = [...state, code];
}
final String encodedData = Code.encode(state);
pref!.setString("favcode", encodedData);
}
}

getx obx not updating avatar image - Flutter GetX

what I want to achieve is to change the image in CircleAvatar when I'm selecting an image, here is the code:
ProfileController:
class ProfileController extends GetxController {
final TextEditingController emailController = TextEditingController();
final ImagePicker _picker = ImagePicker();
Rx<String?> avatarPath = null.obs;
avatarFromCamera() async {
var localAvatar = await _picker.pickImage(
source: ImageSource.camera, imageQuality: 50
);
if (localAvatar != null) {
avatarPath = localAvatar.path.obs;
update();
}
}
avatarFromGallery() async {
var localAvatar = await _picker.pickImage(
source: ImageSource.gallery, imageQuality: 50
);
if (localAvatar != null) {
avatarPath = localAvatar.path.obs;
update();
}
}
String? emailValidator(String? value) {
if (value == null || value.isEmpty) {
return null;
}
if (!EmailValidator.validate(value, false)) {
return 'Invalid email address';
}
}
#override
void onClose() {
emailController.dispose();
super.onClose();
}
String? emailValidator(String? value) {
if (value == null || value.isEmpty) {
return null;
}
if (!EmailValidator.validate(value, false)) {
return 'Invalid email address';
}
}
void save(GlobalKey<FormState> profileFormKey) {
if (profileFormKey.currentState!.validate()) {
print('valid');
}
}
}
and here is the ProfileScreen widget:
lass ProfileScreen extends StatelessWidget {
final ProfileController _profileController = Get.put<ProfileController>(ProfileController());
GlobalKey<FormState> profileFormKey = GlobalKey<FormState>();
ProfileScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Update user details'),
),
body: SingleChildScrollView(
child: Form(
key: profileFormKey,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(30.0),
child: TextFormField(
keyboardType: TextInputType.text,
controller: _profileController.emailController,
decoration: const InputDecoration(
labelText: 'Enter email',
),
validator: _profileController.emailValidator,
),
),
Center(
child: GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
builder: (BuildContext bc) {
return SafeArea(
child: Wrap(
children: <Widget>[
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('Photo Library'),
onTap: () {
_profileController.avatarFromGallery();
Navigator.of(context).pop();
}),
ListTile(
leading: const Icon(Icons.photo_camera),
title: const Text('Camera'),
onTap: () {
_profileController.avatarFromCamera();
Navigator.of(context).pop();
},
),
],
),
);
}
);
},
child: CircleAvatar(
radius: 55,
backgroundColor: Colors.pink,
child: Obx(() =>(_profileController.avatarPath.value != null)
? ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.file(
File(_profileController.avatarPath.value!),
width: 100,
height: 100,
fit: BoxFit.fitHeight
),
)
: Container(
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(50)),
width: 100,
height: 100,
child: Icon(
Icons.camera_alt,
color: Colors.grey[800],
),
),
),
),
)),
Container(
margin: const EdgeInsets.all(10),
width: double.infinity,
child: MaterialButton(
color: Colors.blue,
onPressed: () => _profileController.save(profileFormKey),
child: const Text(
'Submit',
style: TextStyle(color: Colors.white),
),
),
),
],
),
),
),
);
}
}
as you can see, I have Obx and my avatarPath reactive, and I'm running update everywhere I changing it, but it's not udpated. I also tried to use empty string as initial value of imagePath like this Rx<String> avatarPath = ''.obs; and it's not working. What I'm doing wrong??? Thank you in advice!!!
There are two things to revise for it. Firstly, change avatarPath = localAvatar.path.obs; with avatarPath.value = localAvatar.path;. Because localAvatar.path.obs create the new observable and changes will not be reflected to previous observers.
Secondly, create a new stateless widget having the widget tree of bottom sheet's builder like
showModalBottomSheet(
context: context,
builder: (BuildContext bc) {
return CustomBottomView();
}
);
Then inside the CustomBottomView copy your bottom sheet widget tree.
class CustomBottomView extends GetView<ProfileController> {
return YourBottomSheetWidgetTreeHere();
}
Dont worry about ProfileController here. You have already put it in DI in previous route. First follow the first step if you still face the problem second step will definitely resolve it.