I'm having some trouble completing the action related to how I should add/remove (toggle) favorites from a list(coming from an api response). Actually I'm able to add the favorite to the list and also remove it in a separated way, but I can't do it with the same button in a toggled way. In Brief, it is working but I cannot achieve the toggle action properly. Please could you take a look at the code and see if there's a good approach on how to achieve toggling without using setstate?
Button to Dismiss (delete from list)
CupertinoButton(
padding: EdgeInsets.zero,
minSize: 30,
onPressed: () {
onDismissed();
},
child: CircleContainer(
child: Icon(
item.isFavorite
? Icons.favorite
: Icons.favorite_border,
color: Colors.white,
),
size: 35,
),
),
Same Button achieving adding favorite to list with Bloc
CupertinoButton(
padding: EdgeInsets.zero,
minSize: 30,
onPressed: () {
masterBloc.add(MasterAddToFavorites(item));
},
child: CircleContainer(
child: Icon(
item.isFavorite
? Icons.favorite
: Icons.favorite_border,
color: Colors.white,
),
size: 35,
),
),
Item definition
class YoutbeVideo {
final String videoId, title, description, banner;
bool isFavorite;
YoutbeVideo(
{#required this.videoId,
#required this.title,
#required this.description,
#required this.banner,
this.isFavorite});
void toggleFavoriteStatus() {
isFavorite = !isFavorite;
}
BloC Code divided in 3 files
master_bloc
import 'package:bloc/bloc.dart';
import 'package:documentales_app/models/youtube_video.dart';
import 'master_events.dart';
import 'master_state.dart';
class MasterBloc extends Bloc<MasterEvents, MasterState> {
#override
MasterState get initialState => MasterState.initialState();
#override
Stream<MasterState> mapEventToState(MasterEvents event) async* {
if (event is MasterSetTab) {
yield this.state.copyWith(currentTab: event.tab);
} else if (event is MasterAddToHistory) {
yield* _addToHistory(event);
} else if (event is MasterRemoveFromHistory) {
yield* _removeFromHistory(event);
} else if (event is MasterRemoveFromFavorites) {
yield* _removeFromFavorites(event);
} else if (event is MasterLogout) {
yield this.state.copyWith(history: [], currentTab: 0);
} else if (event is MasterAddToFavorites) {
yield* _addToFavorites(event);
}
}
Stream<MasterState> _addToHistory(MasterAddToHistory event) async* {
final int index = this
.state
.history
.indexWhere((item) => item.videoId == event.youtubeVideo.videoId);
if (index == -1) {
final history = List<YoutubeVideo>.from(this.state.history);
history.add(event.youtubeVideo);
yield this.state.copyWith(history: history);
}
}
Stream<MasterState> _addToFavorites(MasterAddToFavorites event) async* {
final int index = this
.state
.favorites
.indexWhere((item) => item.videoId == event.youtubeVideo.videoId);
if (index == -1) {
final favorites = List<YoutubeVideo>.from(this.state.favorites);
favorites.add(event.youtubeVideo);
yield this.state.copyWith(favorites: favorites);
}
}
Stream<MasterState> _removeFromHistory(MasterRemoveFromHistory event) async* {
final history = List<YoutubeVideo>.from(this.state.history);
history.removeAt(event.index);
yield this.state.copyWith(history: history);
}
Stream<MasterState> _removeFromFavorites(
MasterRemoveFromFavorites event) async* {
final favorites = List<YoutubeVideo>.from(this.state.favorites);
favorites.removeAt(event.index);
yield this.state.copyWith(favorites: favorites);
}
}
master state
import 'package:meta/meta.dart' show required;
import 'package:documentales_app/models/youtube_video.dart';
import 'package:equatable/equatable.dart';
class MasterState extends Equatable {
final int currentTab;
final List<YoutubeVideo> history;
final List<YoutubeVideo> favorites;
MasterState(
{#required this.currentTab, #required this.history, this.favorites});
static MasterState initialState() =>
MasterState(currentTab: 0, history: [], favorites: []);
MasterState copyWith(
{int currentTab,
List<YoutubeVideo> history,
List<YoutubeVideo> favorites}) {
return MasterState(
currentTab: currentTab ?? this.currentTab,
history: history ?? this.history,
favorites: favorites ?? this.favorites);
}
#override
List<Object> get props => [currentTab, history, favorites];
}
master events
import 'package:documentales_app/models/youtube_video.dart';
abstract class MasterEvents {}
class MasterSetTab extends MasterEvents {
final int tab;
MasterSetTab(this.tab);
}
class MasterAddToHistory extends MasterEvents {
final YoutubeVideo youtubeVideo;
MasterAddToHistory(this.youtubeVideo);
}
class MasterAddToFavorites extends MasterEvents {
final YoutubeVideo youtubeVideo;
MasterAddToFavorites(this.youtubeVideo);
}
class MasterRemoveFromHistory extends MasterEvents {
final int index;
MasterRemoveFromHistory(this.index);
}
class MasterRemoveFromFavorites extends MasterEvents {
final int index;
MasterRemoveFromFavorites(this.index);
}
So one way to perform a toggle is to replace Add and Remove event with just one. So you could get rid of these events:
class MasterAddToFavorites extends MasterEvents {
final YoutubeVideo youtubeVideo;
MasterAddToFavorites(this.youtubeVideo);
}
class MasterRemoveFromFavorites extends MasterEvents {
final int index;
MasterRemoveFromFavorites(this.index);
}
and replace it with:
class MasterToggleInFavorites extends MasterEvents {
final YoutubeVideo video;
MasterToggleInFavorites(video);
}
Next, inside of bloc, inside of method that handles that event, you could do something like this:
Stream<MasterState> _toggleInFavorites(MasterToggleInFavorites event) async* {
final int index = this
.state
.favorites
.indexWhere((item) => item.videoId == event.youtubeVideo.videoId);
if (index == -1) {
final favorites = List<YoutubeVideo>.from(this.state.favorites);
favorites.add(event.youtubeVideo);
event.youtubeVideo.isFavourite = true;
yield this.state.copyWith(favorites: favorites);
} else {
final favorites = List<YoutubeVideo>.from(this.state.favorites);
favorites.removeAt(index);
event.youtubeVideo.isFavourite = false;
yield this.state.copyWith(favorites: favorites);
}
Video Class
class YoutubeVideo {
final String videoId, title, description, banner;
bool isFavorite;
YoutubeVideo(
{#required this.videoId,
#required this.title,
#required this.description,
#required this.banner,
this.isFavorite = false});
factory YoutubeVideo.fromJson(Map<String, dynamic> json,
{bool fromPlayList = false}) {
final snippet = json['snippet'];
final thumbnail =
snippet['thumbnails']['standard'] ?? snippet['thumbnails']['high'];
String videoId;
if (!fromPlayList) {
videoId = json['contentDetails']['videoId'];
} else {
videoId = snippet['resourceId']['videoId'];
}
return YoutubeVideo(
videoId: videoId,
title: snippet['title'],
description: snippet['description'],
banner: thumbnail['url']);
}
}
And here is the action onPressed but for any reason is not reflecting the color change
CupertinoButton(
padding: EdgeInsets.zero,
minSize: 30,
onPressed: () {
masterBloc.add(MasterToggleInFavorites(item));
},
child: CircleContainer(
child: Icon(
//Icons.playlist_add,
item.isFavorite
? Icons.favorite_border
: Icons.favorite,
color: Colors.white,
),
size: 35,
),
),
Related
I am trying to implement a infinite scroll pagnation in Flutter using provider for state management.
I have two page, HomePage() and TestingPage().On start up, the code is able to successfully load data from the api endpoint but the retrieved data was not displayed by the listview builder.The data is only displayed if i switch to the TestingPage() and back to HomePage().
The code will load more data if i reach the end of the scroll view, but the same problem is happening again, list view builder will not display the newly loaded data. It will only display it if i switch to TestingPage() and back to HomePage().
Reposting this because i made a mistake of posting the wrong code.
// UserInfo.dart
class UserInfo {
int userId;
int id;
String title;
String body;
UserInfo(this.userId, this.id, this.title, this.body);
factory UserInfo.fromJson(Map<String, dynamic> json) {
final userId = json['userId'];
final id = json['id'];
final title = json['title'];
final body = json['body'];
return UserInfo(userId, id, title, body);
}
#override
String toString() {
return "id: $id";
}
}
// user_info_response.dart
import 'package:end_point/models/UserInfo.dart';
class ListOfUserInfoResponse {
int? code;
List<UserInfo> listOfUserInfo;
ListOfUserInfoResponse({this.code, required this.listOfUserInfo});
factory ListOfUserInfoResponse.fromJson(int statusCode, List<dynamic> json) {
List<dynamic> list = json;
return ListOfUserInfoResponse(
code: statusCode,
listOfUserInfo: list.map((i) => UserInfo.fromJson(i)).toList());
}
}
// user_info_api.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:end_point/models/API/user_info_response.dart';
Future<ListOfUserInfoResponse> getListOfUserInfo(page) async {
final response = await http.get(Uri.parse(
"https://jsonplaceholder.typicode.com/posts?_limit=15&_page=$page"));
if (response.statusCode == 200) {
return ListOfUserInfoResponse.fromJson(
response.statusCode, jsonDecode(response.body));
} else {
throw Exception("Failed to load data");
}
}
// user_info_provider.dart
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:end_point/models/UserInfo.dart';
import 'package:end_point/api/user_info_api.dart' as UserInfoAPI;
import 'package:end_point/models/API/user_info_response.dart';
class UserInfoProvider with ChangeNotifier {
int _page = 1;
int get page => _page;
bool _isLoading = false;
bool get isLoading => _isLoading;
List<UserInfo> _listOfUserData = [];
List<UserInfo> get listOfUserData => _listOfUserData;
set listOfUserData(List<UserInfo> listOfUserData) {
_listOfUserData = listOfUserData;
notifyListeners();
}
bool _hasMore = true;
bool get hasMore => _hasMore;
Future<void> loadMoreData() async {
if (_isLoading) return;
_isLoading = true;
ListOfUserInfoResponse response =
await UserInfoAPI.getListOfUserInfo(_page);
_listOfUserData.addAll(response.listOfUserInfo);
_page++;
_isLoading = false;
if (response.listOfUserInfo.length < 20) {
_hasMore = false;
}
notifyListeners();
}
void refreshData() async {
_isLoading = false;
_hasMore = true;
_page = 1;
listOfUserData.clear();
notifyListeners();
}
}
// home.dart
import 'package:end_point/providers/user_info_provider.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'dart:developer';
class HomePage extends StatefulWidget {
const HomePage({super.key});
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _scrollController = ScrollController();
late UserInfoProvider userInfoProvider;
#override
void initState() {
super.initState();
userInfoProvider = Provider.of<UserInfoProvider>(context, listen: false);
userInfoProvider.loadMoreData();
_scrollController.addListener(_onScroll);
}
Future<void> _onScroll() async {
if (_scrollController.position.maxScrollExtent ==
_scrollController.offset) {
await userInfoProvider.loadMoreData();
}
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
Future refresh() async {
userInfoProvider.refreshData();
await userInfoProvider.loadMoreData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("END POINT LEGGO")),
body: RefreshIndicator(
key: UniqueKey(),
onRefresh: refresh,
child: ListView.builder(
key: const PageStorageKey<String>('HP'),
itemCount: userInfoProvider.listOfUserData.length,
controller: _scrollController,
shrinkWrap: true,
itemBuilder: (context, index) {
final id = userInfoProvider.listOfUserData[index].id;
if (index < userInfoProvider.listOfUserData.length) {
return Padding(
padding: const EdgeInsets.all(2.0),
child: Container(
height: 50,
width: 200,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(20)),
color: Colors.red),
child: ListTile(title: Text('$id'))),
);
} else {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: Center(
child: userInfoProvider.hasMore
? const CircularProgressIndicator()
: const Text('No more data to load'),
));
}
}),
));
}
I found the fix to this problem, #home.dart instead of using the UserInfoProvider instance that was declared in the initState().
Simply declare a new userInfoProvider instance inside the Widget build().
E.g. final data = Provider.of(context);
From what I understood, this is because the instance of the UserInfoProvider that was declared in the initState() has listen: false. false value will mean that any value changes, will not trigger a rebuild of the widget.
I am trying to get the user to press a record button, to then speak. His memo is recorded and then when the user tap on the Text widget displaying the name of the audio file, the file should be read so he can listen again to what he has recorded.
When I try, I am getting connection lost and the simulator crash. I do not find what is the problem in my code. I guess it is because I am still new with Flutter. Your help is welcome as I am stuck with this for several days. Many thanks.
import 'dart:io';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_audio_recorder2/flutter_audio_recorder2.dart';
import 'package:path_provider/path_provider.dart';
enum RecordingState {
UnSet,
Set,
Recording,
Stopped,
}
void main66() => runApp(MyTest());
bool isRecording = false;
class MyTest extends StatelessWidget {
const MyTest({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
// backgroundColor: Colors.white,
body: HomeViewRecorder(),//HomeViewRecorder(),
),
);
}
}
class HomeViewRecorder extends StatefulWidget {
final String _title;
const HomeViewRecorder({Key key, String title}) : _title = title,
super(key: key);
#override
_HomeViewRecorderState createState() => _HomeViewRecorderState();
}
class _HomeViewRecorderState extends State<HomeViewRecorder> {
Directory appDirectory;
String records = '';
#override
void initState() {
super.initState();
getApplicationDocumentsDirectory().then((value) {
appDirectory = (value);
setState(() {
print(appDirectory);
});
});
}
void dispose() {
appDirectory.delete();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold (
appBar: AppBar(
title: Text('My recorder Test')),
body: Column(
children: [
Expanded(child:
InkWell(child: RecordListView(records:records)),
),
Center(child: Record_Widget(
onSaved:
_onRecordComplete)),
]
)
);
}
_onRecordComplete() {
records ='';
appDirectory.list().listen((onData) {
if (onData.path.contains('.aac')) records=(onData.path);
}).onDone(() {
setState(() {});
});
}
}
class RecordListView extends StatefulWidget {
final String records;
const RecordListView({
Key key,
this.records,
}) : super(key: key);
#override
_RecordListViewState createState() => _RecordListViewState();
}
class _RecordListViewState extends State<RecordListView> {
int _totalDuration;
int _currentDuration;
double _completedPercentage = 0.0;
bool _isPlaying = false;
int _selectedIndex = -1;
#override
Widget build(BuildContext context){
return Column(
children: [
widget.records.isEmpty?
Text('No records yet'): InkWell(child: Text(widget.records.split("/").last+DateTime.now().toString()),
onTap: _onPlay,),
],
);
}
Future<void> _onPlay({ String filePath, int index}) async {
AudioPlayer audioPlayer = AudioPlayer();
if (!_isPlaying) {
audioPlayer.play(filePath, isLocal: true);
setState(() {
_selectedIndex = index;
_completedPercentage = 0.0;
_isPlaying = true;
});
audioPlayer.onPlayerCompletion.listen((_) {
setState(() {
_isPlaying = false;
_completedPercentage = 0.0;
});
});
audioPlayer.onDurationChanged.listen((duration) {
setState(() {
_totalDuration = duration.inMicroseconds;
});
});
audioPlayer.onAudioPositionChanged.listen((duration) {
setState(() {
_currentDuration = duration.inMicroseconds;
_completedPercentage =
_currentDuration.toDouble() / _totalDuration.toDouble();
});
});
}
}
String _getDateFromFilePath({ String filePath}) {
String fromEpoch = filePath.substring(
filePath.lastIndexOf('/') + 1, filePath.lastIndexOf('.'));
DateTime recordedDate =
DateTime.fromMillisecondsSinceEpoch(int.parse(fromEpoch));
int year = recordedDate.year;
int month = recordedDate.month;
int day = recordedDate.day;
return ('$year-$month-$day');
}
}
class Record_Widget extends StatefulWidget {
final Function onSaved;
const Record_Widget({Key key,
this.onSaved,}) : super(key: key);
#override
_Record_WidgetState createState() => _Record_WidgetState();
}
class _Record_WidgetState extends State<Record_Widget> {
Directory appDirectory;
String records = '';
RecordingState _recordingState = RecordingState.UnSet;
// Recorder properties
FlutterAudioRecorder2 audioRecorder;
#override
void initState() {
super.initState();
FlutterAudioRecorder2.hasPermissions.then((hasPermission) {
if (hasPermission) {
_recordingState = RecordingState.Set;
}
});
}
#override
void dispose() {
_recordingState = RecordingState.UnSet;
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(100.0),
child: _RecordOrStopButton());
}
Container _RecordOrStopButton() {
return Container(
height: 90,
width: 90,
decoration: BoxDecoration(
border: Border.all(
width: 4.0,
color: Colors.grey,
),
shape: BoxShape.circle,
),
child: Padding(
padding: EdgeInsets.all(4.0),
child: isRecording == false ?
_createRecordButton() : _createStopButton()),);
}
//Widget Button Record
Container _createRecordButton({IconData icon, Function onPressFunc}) {
return Container(child: ElevatedButton(
onPressed: () async {
await _onRecordButtonPressed();
setState(() {
_recordingState = RecordingState.Recording;
isRecording = true;
});
},
style: ButtonStyle(
shape: MaterialStateProperty.all(CircleBorder()),
padding: MaterialStateProperty.all(EdgeInsets.all(20)),
backgroundColor: MaterialStateProperty.all(
Colors.red), // <-- Button color
)));
}
//Widget Button Stop
Container _createStopButton() {
return Container(
padding: EdgeInsets.all(16),
width: 30.0,
height: 30.0,
child: ElevatedButton(
onPressed: () {
_onRecordButtonPressed();
setState(() {
isRecording = false;
});
},
style: ButtonStyle(
fixedSize: MaterialStateProperty.all(Size(10, 10)),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(color: Colors.red))),
padding: MaterialStateProperty.all(EdgeInsets.all(20)),
backgroundColor: MaterialStateProperty.all(Colors.red),
),
));
}
Future<void> _onRecordButtonPressed() async {
switch (_recordingState) {
case RecordingState.Set:
await _recordVoice();
break;
case RecordingState.Recording:
await _stopRecording();
_recordingState = RecordingState.Stopped;
break;
case RecordingState.Stopped:
await _recordVoice();
break;
case RecordingState.UnSet:
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Please allow recording from settings.'),
));
break;
}
}
_initRecorder() async {
Directory appDirectory = await getApplicationDocumentsDirectory();
String filePath = appDirectory.path +
'/' +
DateTime.now().millisecondsSinceEpoch.toString() +
'.aac';
audioRecorder =
FlutterAudioRecorder2(filePath, audioFormat: AudioFormat.AAC);
await audioRecorder.initialized;
}
_startRecording() async {
isRecording = true;
await audioRecorder.start();
await audioRecorder.current(channel: 0);
}
_stopRecording() async {
isRecording = false;
await audioRecorder.stop();
widget.onSaved();
}
Future<void> _recordVoice() async {
final hasPermission = await FlutterAudioRecorder2.hasPermissions;
if (hasPermission ?? false) {
await _initRecorder();
await _startRecording();
_recordingState = RecordingState.Recording;
} else {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Please allow recording from settings.'),
));
}
}
}
I tried to migrate the no null safety code to null safety and I ended up with errors. I want to get autocomplete location of places in Flutter and display details on the tapped place.
Screenshots of errors:
The code:
main.dart
import 'package:flutter/material.dart';
import 'package:google_places_flutter/address_search.dart';
import 'package:google_places_flutter/place_service.dart';
import 'package:uuid/uuid.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Google Places Demo',
home: MyHomePage(title: 'Places Autocomplete Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, this.title}) : super(key: key);
final String? title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _controller = TextEditingController();
String? _streetNumber = '';
String? _street = '';
String? _city = '';
String? _zipCode = '';
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
body: Container(
margin: EdgeInsets.only(left: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextField(
controller: _controller,
readOnly: true,
onTap: () async {
// generate a new token here
final sessionToken = Uuid().v4();
final Suggestion? result = await showSearch(
context: context,
delegate:AddressSearch(sessionToken),
);
// This will change the text displayed in the TextField
if (result != null) {
final placeDetails = await PlaceApiProvider(sessionToken)
.getPlaceDetailFromId(result.placeId);
setState(() {
_controller.text = result.description!;
_streetNumber = placeDetails.streetNumber;
_street = placeDetails.street;
_city = placeDetails.city;
_zipCode = placeDetails.zipCode;
});
}
},
decoration: InputDecoration(
icon: Container(
width: 10,
height: 10,
child: Icon(
Icons.home,
color: Colors.black,
),
),
hintText: "Enter address",
border: InputBorder.none,
contentPadding: EdgeInsets.only(left: 8.0, top: 16.0),
),
),
SizedBox(height: 20.0),
Text('Street Number: $_streetNumber'),
Text('Street: $_street'),
Text('City: $_city'),
Text('ZIP Code: $_zipCode'),
],
),
),
);
}
}
address_search.dart
import 'package:flutter/material.dart';
import 'package:google_places_flutter/place_service.dart';
class AddressSearch extends SearchDelegate<Suggestion?> {
AddressSearch(this.sessionToken) {
apiClient = PlaceApiProvider(sessionToken);
}
final sessionToken;
late PlaceApiProvider apiClient;
#override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
tooltip: 'Clear',
icon: Icon(Icons.clear),
onPressed: () {
query = '';
},
)
];
}
#override
Widget buildLeading(BuildContext context) {
return IconButton(
tooltip: 'Back',
icon: Icon(Icons.arrow_back),
onPressed: () {
close(context, null);
},
);
}
#override
Widget buildResults(BuildContext context) {
return Container();
}
#override
Widget buildSuggestions(BuildContext context) {
return FutureBuilder(
future: query == ""
? null
: apiClient.fetchSuggestions(
query, Localizations.localeOf(context).languageCode),
builder: (context, snapshot) => query == ''
? Container(
padding: EdgeInsets.all(16.0),
child: Text('Enter address'),
)
: snapshot.hasData
? ListView.builder(
itemBuilder: (context, index) =>
ListTile(
title:
Text((snapshot.data[index] as Suggestion).description!),
onTap: () {
close(context, snapshot.data[index] as Suggestion?);
},
),
itemCount: snapshot.data.length,
)
: Container(child: Text('Loading...')),
);
}
}
place_service.dart
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart';
class Place {
String? streetNumber;
String? street;
String? city;
String? zipCode;
Place({
this.streetNumber,
this.street,
this.city,
this.zipCode,
});
#override
String toString() {
return 'Place(streetNumber: $streetNumber, street: $street, city: $city, zipCode: $zipCode)';
}
}
class Suggestion {
final String? placeId;
final String? description;
Suggestion(this.placeId, this.description);
#override
String toString() {
return 'Suggestion(description: $description, placeId: $placeId)';
}
}
class PlaceApiProvider {
final client = Client();
PlaceApiProvider(this.sessionToken);
final sessionToken;
static final String androidKey = 'YOUR_API_KEY_HERE';
static final String iosKey = 'YOUR_API_KEY_HERE';
final apiKey = Platform.isAndroid ? androidKey : iosKey;
Future<List<Suggestion>?> fetchSuggestions(String input, String lang) async {
final request =
'https://maps.googleapis.com/maps/api/place/autocomplete/json?input=$input&key=$apiKey&sessiontoken=$sessionToken';
final response = await client.get(Uri.parse(request));
if (response.statusCode == 200) {
final result = json.decode(response.body);
if (result['status'] == 'OK') {
// compose suggestions in a list
return result['predictions']
.map<Suggestion>((p) => Suggestion(p['place_id'], p['description']))
.toList();
}
if (result['status'] == 'ZERO_RESULTS') {
return [];
}
throw Exception(result['error_message']);
} else {
throw Exception('Failed to fetch suggestion');
}
}
Future<Place> getPlaceDetailFromId(String? placeId) async {
final request =
'https://maps.googleapis.com/maps/api/place/details/json?place_id=$placeId&fields=address_component&key=$apiKey&sessiontoken=$sessionToken';
final response = await client.get(Uri.parse(request));
if (response.statusCode == 200) {
final result = json.decode(response.body);
if (result['status'] == 'OK') {
final components =
result['result']['address_components'] as List<dynamic>;
// build result
final place = Place();
components.forEach((c) {
final List type = c['types'];
if (type.contains('street_number')) {
place.streetNumber = c['long_name'];
}
if (type.contains('route')) {
place.street = c['long_name'];
}
if (type.contains('locality')) {
place.city = c['long_name'];
}
if (type.contains('postal_code')) {
place.zipCode = c['long_name'];
}
});
return place;
}
throw Exception(result['error_message']);
} else {
throw Exception('Failed to fetch suggestion');
}
}
}
The solution is probably this:
builder: (context, AsyncSnapshot<List<Suggestion>> snapshot)
instead of this:
builder: (context, snapshot)
then you can do something like:
List<Suggestion>? suggestions = snapshot.data;
if ( suggestions != null && suggestions.length > 0) {
Hey i make a Favorite System with a bool to say if is favorite or not.
But if the name of the bool is always the same it applies to all my entries!
but each entry has its own name (widget.name), and i thought maybe something like that could work
bool widget.name;
but this not work :(
how can i solve that each entry has its own bool?
by the way i use this plugin for that
https://pub.dev/packages/shared_preferences/example
SharedPreferences sharedPreferences;
bool isfavorit;
#override
void initState() {
super.initState();
SharedPreferences.getInstance().then((SharedPreferences sp) {
sharedPreferences = sp;
isfavorit = sharedPreferences.getBool(spKey);
// will be null if never previously saved
if (isfavorit == null) {
isfavorit = false;
persist(isfavorit); // set an initial value
}
setState(() {});
});
}
void persist(bool value) {
setState(() {
isfavorit = value;
});
sharedPreferences?.setBool(spKey, value);
}
Complete Code
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class Details extends StatefulWidget {
final String name;
Details(
this.name,
);
#override
_DetailsState createState() => _DetailsState();
}
const String spKey = 'myBool';
class _DetailsState extends State<Details> {
SharedPreferences sharedPreferences;
bool isfavorit;
#override
void initState() {
super.initState();
SharedPreferences.getInstance().then((SharedPreferences sp) {
sharedPreferences = sp;
isfavorit = sharedPreferences.getBool(spKey);
// will be null if never previously saved
if (isfavorit == null) {
isfavorit = false;
persist(isfavorit); // set an initial value
}
setState(() {});
});
}
void persist(bool value) {
setState(() {
isfavorit = value;
});
sharedPreferences?.setBool(spKey, value);
}
// ignore: missing_return
IconData favicon() {
if (isfavorit == true) {
return Icons.favorite;
} else if (isfavorit == false) {
return Icons.favorite_border;
}
}
// ignore: missing_return
Color favicolor() {
if (isfavorit == true) {
return Colors.red;
} else if (isfavorit == false) {
return Colors.white;
}
}
void changefav() {
if (isfavorit == true) {
return persist(false);
} else if (isfavorit == false) {
return persist(true);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
icon: Icon(
favicon(),
color: favicolor(),
),
onPressed: () => changefav(),
),
],
title: Text(widget.name),
),
body: Container(
child: Text(widget.name),
),
);
}
}
You are always saving the isFavorite to the same key in shared preferences, instead of using a constant key use one that is based on the widget.name
So for instance:
sharedPreferences.getBool('details_favorite_${widget.name}');
I'm using the youtube api and placed a favorites toggle button to fill and empty the icon depending on the state and saving them in ListView in a new page called 'favorites'. Everything is working fine in the new page as I can see the favorite Icon filled, but is not being refreshed/updated in real time in the current view. If I switch to a statefulWidget, I'm able to make it work using 'setstate', but then the changes are not reflected if I empty the icons from the favorites page.
There must be something wrong in my approach as I should use the Bloc state to change both, but really stuck here. Please could you take a look at my code and give me some thoughts or ideas?
Bloc File
class MasterBloc extends Bloc<MasterEvents, MasterState> {
#override
MasterState get initialState => MasterState.initialState();
#override
Stream<MasterState> mapEventToState(MasterEvents event) async* {
if (event is MasterSetTab) {
yield this.state.copyWith(currentTab: event.tab);
} else if (event is MasterAddToHistory) {
yield* _addToHistory(event);
} else if (event is MasterRemoveFromHistory) {
yield* _removeFromHistory(event);
} else if (event is MasterToggleInFavorites) {
yield* _toggleInFavorites(event);
} else if (event is MasterLogout) {
yield this.state.copyWith(history: [], currentTab: 0);
}
}
Stream<MasterState> _addToHistory(MasterAddToHistory event) async* {
final int index = this
.state
.history
.indexWhere((item) => item.videoId == event.youtubeVideo.videoId);
if (index == -1) {
final history = List<YoutubeVideo>.from(this.state.history);
history.add(event.youtubeVideo);
yield this.state.copyWith(history: history);
}
}
Stream<MasterState> _removeFromHistory(MasterRemoveFromHistory event) async* {
final history = List<YoutubeVideo>.from(this.state.history);
history.removeAt(event.index);
yield this.state.copyWith(history: history);
}
Stream<MasterState> _toggleInFavorites(MasterToggleInFavorites event) async* {
final int index = this
.state
.favorites
.indexWhere((item) => item.videoId == event.youtubeVideo.videoId);
if (index == -1) {
final favorites = List<YoutubeVideo>.from(this.state.favorites);
favorites.add(event.youtubeVideo);
event.youtubeVideo.isFavorite = true;
yield this.state.copyWith(favorites: favorites);
} else {
final favorites = List<YoutubeVideo>.from(this.state.favorites);
favorites.removeAt(index);
event.youtubeVideo.isFavorite = false;
yield this.state.copyWith(favorites: favorites);
}
}
}
Bloc State
class MasterState extends Equatable {
final int currentTab;
final List<YoutubeVideo> history;
final List<YoutubeVideo> favorites;
MasterState(
{#required this.currentTab, #required this.history, this.favorites});
static MasterState initialState() =>
MasterState(currentTab: 0, history: [], favorites: []);
MasterState copyWith(
{int currentTab,
List<YoutubeVideo> history,
List<YoutubeVideo> favorites}) {
return MasterState(
currentTab: currentTab ?? this.currentTab,
history: history ?? this.history,
favorites: favorites ?? this.favorites);
}
#override
List<Object> get props => [currentTab, history, favorites];
}
BloC Events
import 'package:documentales_app/models/youtube_video.dart';
abstract class MasterEvents {}
class MasterSetTab extends MasterEvents {
final int tab;
MasterSetTab(this.tab);
}
class MasterAddToHistory extends MasterEvents {
final YoutubeVideo youtubeVideo;
MasterAddToHistory(this.youtubeVideo);
}
class MasterRemoveFromHistory extends MasterEvents {
final int index;
MasterRemoveFromHistory(this.index);
}
class MasterToggleInFavorites extends MasterEvents {
final YoutubeVideo youtubeVideo;
MasterToggleInFavorites(this.youtubeVideo);
}
class MasterLogout extends MasterEvents {}
Favorites Tab
class FavsTab extends StatefulWidget {
#override
_FavsTabState createState() => _FavsTabState();
}
class _FavsTabState extends State<FavsTab> {
#override
Widget build(BuildContext context) {
final bloc = BlocProvider.of<MasterBloc>(context);
return BlocBuilder<MasterBloc, MasterState>(
builder: (_, state) {
if (state.favorites.length == 0) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset('assets/icons/empty.svg',
width: 50, color: Colors.greenAccent),
SizedBox(
height: 5,
),
Text(
'No hay favoritos ..',
style: TextStyle(
color: Colors.greenAccent,
fontWeight: FontWeight.bold,
fontSize: 20),
)
],
),
);
}
return ListView.builder(
itemBuilder: (_, index) {
final YoutubeVideo item = state.favorites[index];
return YoutubeVideoItem(
item: item,
onDismissed: () {
bloc.add(MasterToggleInFavorites(item));
},
);
},
itemCount: state.favorites.length,
);
},
condition: (prevState, newState) =>
prevState.favorites.length != newState.favorites.length,
);
}
}
Home Tab where the state is called
class HomeTab extends StatefulWidget {
#override
_HomeTabState createState() => _HomeTabState();
}
class _HomeTabState extends State<HomeTab> {
AccountApi _accountApi = AccountApi();
YoutubeApi _youtubeApi = YoutubeApi(apiKey: API_KEY);
List<dynamic> _users = [];
List<PlayList> _playlists = [];
List<YoutubeVideo> _newVideos = [];
bool _isLoading = true;
#override
void initState() {
super.initState();
_load();
}
_load() async {
final users = await _accountApi.getUsers(1);
final List<PlayList> playLists =
await _youtubeApi.getPlaylists('UCCMksip5JfLMW4AJGsjTYUA');
final List<YoutubeVideo> newVideos = await _youtubeApi
.getPlaylistVideos('PLFXLg_sVKmuujWVeOmrzsM1NnDFa8uoNk');
setState(() {
_users.addAll(users);
_playlists.addAll(playLists);
_newVideos.addAll(newVideos);
_isLoading = false;
});
}
#override
Widget build(BuildContext context) {
return ListView(
children: [
_isLoading
? HomeTabShimmer()
: Column(
children: [
TopPlayLists(items: _playlists),
SizedBox(height: 10),
NewVideos(
items: _newVideos,
),
SizedBox(height: 5),
],
)
],
);
}
}
Finally the toggle button
CupertinoButton(
padding: EdgeInsets.zero,
minSize: 30,
onPressed: () {
masterBloc.add(MasterToggleInFavorites(item));
},
child: CircleContainer(
child: Icon(
//Icons.playlist_add,
item.isFavorite
? Icons.favorite
: Icons.favorite_border,
color: Colors.white,
),
size: 35,
),
),
You can update the Toggle inside a BlocBuilder - the widget rebuilds on state changes. If there's a state change that you'd like to observe in your bloc, calling something like context.read<YourBloc>().doSomething() should update the widgets inside BlocBuilder.
Using BlocListener is another approach that you can use.