I am confused that will Nested ProviderScope and all Providers be romoved from memory? And is following usecase good practice or bad practice?
I have idsProvider
final idsProvider = Provider((_) => List.generate(50, (i) => i));
and have itemIdProvider for every id of idsProvider
final itemIdProvider = Provider.autoDispose((_) => 0);
UI as follows:
class BuildListView extends ConsumerWidget {
const BuildListView({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
final ids = ref.watch(idsProvider);
return ListView.builder(
itemCount: ids.length,
itemBuilder: (context, index) {
return ProviderScope(
overrides: [
itemIdProvider.overrideWithValue(ids[index]),
],
child: const BuildItem(),
);
},
);
}
}
class BuildItem extends ConsumerWidget {
const BuildItem({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
final itemState = ref.watch(itemProvider);
return itemState.when(
data: (id, data) => ListTile(
title: Text("ID: $id"),
subtitle: Text(data),
),
loading: () => const CircularProgressIndicator(),
error: (error) => Text(error.toString()),
);
}
}
Then I have stateNotifierProvider to manipulate the state of every item of the ListView:
final itemProvider = StateNotifierProvider.autoDispose<ItemNotifier, ItemState>(
(ref) => ItemNotifier(ref.watch(itemIdProvider)),
dependencies: [itemIdProvider],
);
class ItemNotifier extends StateNotifier<ItemState> {
ItemNotifier(this.id) : super(const ItemState.loading()) {
fetchData();
}
final int id;
Future<void> fetchData() async {
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
state = ItemState.data(id: id, data: "Data for $id");
}
}
// A lot of methods to change the state
// ...
// ...
}
#freezed
class ItemState with _$ItemState {
const factory ItemState.data({required int id, required String data}) = Data;
const factory ItemState.loading() = Loading;
const factory ItemState.error([String? message]) = Error;
}
I think it's perfectly acceptable. In addition, you may not have an initial value:
final itemIdProvider = Provider.autoDispose((_) => throw UnimplementedError());
This way it will be seen that the value will be implemented later.
About memory. ProviderScope is a StatefulWidget and has the following lines of code under the 'hood':
#override
void dispose() {
container.dispose();
super.dispose();
}
So you don't have to worry too much :)
Related
I have a Flutter page that makes use of 2 data sources: one from API (Internet) and one from Shared Preferences. The API source has no problem, as I used FutureBuilder in the build() method. For the Shared Preferences, I have no idea how to apply another Future Builder (or should I add one more?). Here are the codes (I tried to simplify them):
Future<List<City>> fetchCities(http.Client client) async {
final response = await client
.get(Uri.parse('https://example.com/api/'));
return compute(parseCities, response.body);
}
List<City> parseCities(String responseBody) {
final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<City>((json) => City.fromJson(json)).toList();
}
class CityScreen extends StatelessWidget {
static const routeName = '/city';
const CityScreen({super.key, required this.title});
final String title;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: FutureBuilder<List<City>>(
future: fetchCities(http.Client()),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(snapshot.error.toString()),
);
} else if (snapshot.hasData) {
return CityList(cities: snapshot.data!);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
)
);
}
}
class CityList extends StatefulWidget {
const CityList({super.key, required this.cities});
final List<City> cities;
#override
State<CityList> createState() => _CityListState();
}
class _CityListState extends State<CityList> {
List<String> completedMissionIDs = [];
#override
void initState() {
super.initState();
Player.loadMissionStatus().then((List<String> result) {
setState(() {
completedMissionIDs = result;
if (kDebugMode) {
print(completedMissionIDs);
}
});
});
}
#override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: widget.cities.length * 2,
itemBuilder: (context, i) {
if (i.isOdd) return const Divider();
final index = i ~/ 2;
double completedPercent = _calculateCompletionPercent(widget.cities[index].missionIDs, completedMissionIDs);
return ListTile(
leading: const Icon(Glyphicon.geo, color: Colors.blue),
title: Text(widget.cities[index].cityName),
trailing: Text('$completedPercent%'),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => MissionScreen(title: '${widget.cities[index].cityName} Missions', cityId: widget.cities[index].id),
)
);
},
);
},
);
}
double _calculateCompletionPercent<T>(List<T> cityMissionList, List<T> completedList) {
if(cityMissionList.isEmpty) {
return 0;
}
int completedCount = 0;
for (var element in completedList) {
if(cityMissionList.contains(element)) {
completedCount++;
}
}
if (kDebugMode) {
print('Completed: $completedCount, Total: ${cityMissionList.length}');
}
return completedCount / cityMissionList.length;
}
}
The problem is, the build function in the _CityListState loads faster than the Player.loadMissionStatus() method in the initState, which loads a List<int> from shared preferences.
The shared preferences are loaded in the midway of the ListTiles are generated, making the result of completedPercent inaccurate. How can I ask the ListTile to be built after the completedPercent has been built?
Thanks.
I would start by making CityList a StatelessWidget that accepts completedMissionIDs as a constructor parameter.
Your CityScreen widget can call both APIs and combine the results into a single Future. Pass the combined Future to your FutureBuilder. That way you can render the CityList once all of the data has arrived from both APIs.
I put together a demo below:
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(
home: CityScreen(title: 'City Screen'),
));
}
class CombinedResult {
final List<City> cities;
final List<int> status;
const CombinedResult({
required this.cities,
required this.status,
});
}
class City {
final String cityName;
final List<int> missionIDs;
const City(this.cityName, this.missionIDs);
}
class Player {
static Future<List<int>> loadMissionStatus() async {
await Future.delayed(const Duration(seconds: 1));
return [0, 3];
}
}
Future<List<City>> fetchCities() async {
await Future.delayed(const Duration(seconds: 2));
return const [
City('Chicago', [1, 2, 3, 4]),
City('Helsinki', [1, 2, 3, 4]),
City('Kathmandu', [0, 4]),
City('Seoul', [1, 2, 3]),
];
}
class CityScreen extends StatefulWidget {
const CityScreen({super.key, required this.title});
final String title;
#override
State<CityScreen> createState() => _CityScreenState();
}
class _CityScreenState extends State<CityScreen> {
late Future<CombinedResult> _future;
#override
void initState() {
super.initState();
_future = _fetchData();
}
Future<CombinedResult> _fetchData() async {
final cities = await fetchCities();
final status = await Player.loadMissionStatus();
return CombinedResult(cities: cities, status: status);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: FutureBuilder<CombinedResult>(
future: _future,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(snapshot.error.toString()),
);
} else if (snapshot.hasData) {
return CityList(
cities: snapshot.data!.cities,
completedMissionIDs: snapshot.data!.status,
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}
}
class CityList extends StatelessWidget {
const CityList({
super.key,
required this.cities,
required this.completedMissionIDs,
});
final List<City> cities;
final List<int> completedMissionIDs;
#override
Widget build(BuildContext context) {
return ListView.separated(
padding: const EdgeInsets.all(16.0),
itemCount: cities.length,
separatorBuilder: (context, i) => const Divider(),
itemBuilder: (context, i) => ListTile(
leading: const Icon(Icons.location_city, color: Colors.blue),
title: Text(cities[i].cityName),
trailing: Text(
'${_calculateCompletionPercent(cities[i].missionIDs, completedMissionIDs)}%'),
),
);
}
double _calculateCompletionPercent<T>(
List<T> cityMissionList, List<T> completedList) =>
completedList.where(cityMissionList.contains).length /
cityMissionList.length;
}
First of all I would separate the data layer from the presentation. Bloc would be one example.
To combine 2 Futures you could do something like
final multiApiResult = await Future.wait([
sharedPrefs.get(),
Player.loadMissionStatus()
])
I have a piece of code to scan and read device information. I have printed the elements in the list in onScan function, however I don't know how to get that information and put it in a listview.
Can someone help me?
List<Data> listDevice = [];
Future<void> getData() async {
var apiEndpoint = TTAPI.shared;
await apiEndpoint.devideScan(((data) => onScan(data)));
}
Future<void> onScan(dynamic data) async {
var dataResponse = DataResponse.fromJson(data);
print(dataResponse.toJson());
List<dynamic> dt = jsonDecode(jsonEncode(dataResponse.data).toString());
dt.forEach((element) {
var item = Data.fromJson(element);
print(item.modelName);
listDevice.add(item);
});
var connectRequest = {
'serialNumber': 'DEVICE_SERIAL',
'modelName': 'DEVICE_MODEL',
'ipAddr': 'DEVICE_IP'
};
var apiEndpoint = TTAPI.shared;
await apiEndpoint.connectDevice(connectRequest);
}
Future<List<Data>> getList() async {
return listDevice;
}
You can see more of my code here: https://docs.google.com/document/d/1ntxaDpyNCLD1MyzJOTmZsrh7-Jfim8cm0Va86IQZGww/edit?usp=sharing
As for the current code structure, listDevice is populated inside Future. So you can call setState to update the UI after getting the list at the end of onScan.
Future<void> getData() async {
var apiEndpoint = TTAPI.shared;
await apiEndpoint.devideScan(((data) => onScan(data)));
setState((){});
}
But it would be great to use FutureBuilder and return list from getData.
Current question pattern example
class TextFW extends StatefulWidget {
const TextFW({super.key});
#override
State<TextFW> createState() => _TextFWState();
}
class _TextFWState extends State<TextFW> {
//for current question way
List<int> listDevice = [];
Future<void> getData() async {
await Future.delayed(Duration(seconds: 2));
/// others async method
listDevice = List.generate(10, (index) => index);
setState(() {}); //here or `getData().then()`
}
#override
void initState() {
super.initState();
getData();
// or this getData().then((value) => setState((){}));
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: listDevice.length,
itemBuilder: (context, index) => ListTile(
title: Text("${listDevice[index]}"),
),
),
);
}
}
Using FutureBuilder
class TextFW extends StatefulWidget {
const TextFW({super.key});
#override
State<TextFW> createState() => _TextFWState();
}
class _TextFWState extends State<TextFW> {
/// method will provide data by scanning
Future<List<int>> getData() async {
await Future.delayed(Duration(seconds: 2));
return List.generate(10, (index) => index);
}
late final fututre = getData();
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<List<int>>(
future: fututre,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text("${snapshot.error}");
}
if (snapshot.hasData) {
final listDevice = snapshot.data;
return ListView.builder(
itemCount: listDevice?.length,
itemBuilder: (context, index) => ListTile(
title: Text("${listDevice![index]}"),
),
);
}
return CircularProgressIndicator();
},
),
);
}
}
I was looking for alternatives in which the data is loaded from API only once and stays that way if I move to and fro that screen and I found one in using InheritedWidget. However, I'm now getting the below error and I cannot figure out how to get rid of this.
The getter 'users' was called on null.
Receiver: null
Tried calling: users
The errors are marked as comments in the below:
My code:
InheritedWidget Class
class InheritedUsers extends InheritedWidget {
final UsersList users;
InheritedUsers({required Key key, required Widget child})
: assert(child != null),
users = UsersList(),
super(key: key, child: child);
static UsersList of(BuildContext context) =>
(context.dependOnInheritedWidgetOfExactType(aspect: InheritedUsers)
as InheritedUsers)
.users;
#override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
}
class UsersList {
late List<User> listOfUsers;
Future<List<User>> get userList async {
return listOfUsers = await UsersApi.getUsers();
}
}
class UsersApi with ChangeNotifier {
static Future<List<User>> getUsers() async {
// List<User> list = [];
// late final body;
final url =
'https://firebasestorage.googleapis.com/v0/b/web-johannesmilke.appspot.com/o/other%2Fvideo126%2Fusers.json?alt=media';
final response = await http.get(Uri.parse(url));
final body = json.decode(response.body);
return body.map<User>(User.fromJson).toList();
}
}
UserNetworkPage widget
class UserNetworkPage extends StatefulWidget {
UserNetworkPageState createState() => UserNetworkPageState();
}
class UserNetworkPageState extends State<UserNetworkPage> {
late final Future<List<User>> result;
#override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
result = InheritedUsers.of(context).userList; //This is where the error gets thrown
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) => Scaffold(
body: FutureBuilder<List<User>>(
future: result,
builder: (context, snapshot) {
final users = snapshot.data;
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
default:
if (snapshot.hasError) {
return Center(child: Text('Some error occurred!'));
} else {
return buildUsers(users!);
}
}
},
),
);
Widget buildUsers(List<User> users) => ListView.builder(
physics: BouncingScrollPhysics(),
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) => UserPage(user),
)),
leading: CircleAvatar(
backgroundImage: NetworkImage(user.urlAvatar),
),
title: Text(user.username),
subtitle: Text(user.email),
);
},
);
}
What you need at this point is a state management solution. There's a lot of them, and I'm pretty sure they all use InheritedWidget underneath.
Introduction to state management
List of state management approaches
I personally recommend Riverpod, Provider or BLoC.
I'm a little stuck and can't figure out how the architectural flow should work in this use case. I know almost nothing about functional programming, I'm using this Either from dartz package, a functional programming package. Can someone help me with the following:
I want to show a popup dialog instead of a widget if there is an error. But the design of Either seems to not allow this somehow as this if logic requires a widget of course. Is there a better design which I could accomplish this with?
Learning error handling here
import 'dart:convert';
import 'dart:io';
import 'package:dartz/dartz.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:awesome_dialog/awesome_dialog.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.orange,
),
home: const HomePage(),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.blueGrey,
body: Flex(
direction: Axis.horizontal,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Consumer(
builder: (ctx, ref, child) {
if (ref.watch(notifier).state == NotifierState.initial) {
return const Text('Press the button');
} else if (ref.watch(notifier).state == NotifierState.loading) {
return const CircularProgressIndicator();
} else {
return ref.read(notifier).post.fold(
(failure) {
showDialog( /// Error here, expects a widget
context: context,
barrierDismissible: true,
builder: (BuildContext context) => AlertDialog(
title: Text(failure.toString()),
),
);
},
(post) => Text(post.toString()),
);
}
},
),
Consumer(
builder: (ctx, ref, child) {
return ElevatedButton(
onPressed: () {
ref.read(notifier).getOnePost();
},
child: const Text('Get Post'));
},
),
],
),
),
],
),
);
}
}
class PostService {
final httpClient = FakeHttpClient();
Future<Post?> getOnePost() async {
try {
final responseBody = await httpClient.getResponseBody();
return Post.fromJson(responseBody);
} on SocketException {
throw Failure('No Internet connection 😑');
} on HttpException {
throw Failure("Couldn't find the post 😱");
} on FormatException {
throw Failure("Bad response format 👎");
}
}
}
class FakeHttpClient {
Future<String> getResponseBody() async {
await Future.delayed(const Duration(milliseconds: 500));
//! No Internet Connection
// throw SocketException('No Internet');
//! 404
throw HttpException('404');
//! Invalid JSON (throws FormatException)
// return 'abcd';
// return '{"userId":1,"id":1,"title":"nice title","body":"cool body"}';
}
}
enum NotifierState { initial, loading, loaded }
final notifier = ChangeNotifierProvider((ref) => PostChangeNotifier());
class PostChangeNotifier extends ChangeNotifier {
final _postService = PostService();
NotifierState _state = NotifierState.initial;
NotifierState get state => _state;
void _setState(NotifierState state) {
_state = state;
notifyListeners();
}
late Either<Failure, Post?> _post;
Either<Failure, Post?> get post => _post;
// Set post
void _setPost(Either<Failure, Post?> post) {
_post = post;
notifyListeners();
}
// Set one post
void getOnePost() async {
_setState(NotifierState.loading);
await Task(() => _postService.getOnePost())
.attempt()
.mapLeftToFailure()
.run()
.then((value) => _setPost(value as Either<Failure, Post?>));
_setState(NotifierState.loaded);
}
}
extension TaskX<T extends Either<Object, U>, U> on Task<T> {
Task<Either<Failure, U>> mapLeftToFailure() {
return map(
(either) => either.leftMap((obj) {
try {
return obj as Failure;
} catch (e) {
throw obj;
}
}),
);
}
}
class Post {
final int id;
final int userId;
final String title;
final String body;
Post({
required this.id,
required this.userId,
required this.title,
required this.body,
});
static Post? fromMap(Map<String, dynamic> map) {
return Post(
id: map['id'],
userId: map['userId'],
title: map['title'],
body: map['body'],
);
}
static Post? fromJson(String source) => fromMap(json.decode(source));
#override
String toString() {
return 'Post id: $id, userId: $userId, title: $title, body: $body';
}
}
class Failure {
// Use something like "int code;" if you want to translate error messages
final String message;
Failure(this.message);
#override
String toString() => message;
}
you don't call a function instead of widget, You should call class and initialize your dialog in initState
// call show dialog
(failure) {
ShowDialogScreen(failure: failure.toString());
},
// show dialog screen
class ShowDialogScreen extends StatefulWidget {
final String failure;
const ShowDialogScreen({Key key, this.failure}) : super(key: key);
#override
_ShowDialogScreenState createState() => _ShowDialogScreenState();
}
class _ShowDialogScreenState extends State<ShowDialogScreen> {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) => AlertDialog(
title: Text(widget.failure),
),
);
});
}
#override
Widget build(BuildContext context) {
return Container();
}
}
class AdView extends StatefulWidget {
const AdView({Key? key, required String id}) : super(key: key);
final id = '2';
#override
_AdViewState createState() => _AdViewState();
}
class _AdViewState extends State<AdView> {
final _adService = NewsService();
late Future<Categories> _futureCategories;
late Future<AdBanner> _futureBanners;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
getData() async {
_futureCategories = _adService.getAllCategories();
_futureBanners = _adService.getAds('2');
AdBanner foos;
Categories bars;
await Future.wait<void>([
_futureBanners.then((result) => foos = result),
_futureCategories.then((result) => bars = result),
]);
}
return FutureBuilder(
future: getData(),
builder: (BuildContext context, AsyncSnapshot<dynamic> shot) {
// (BuildContext context, AsyncSnapshot<List<dynamic>> shot) {
if (shot.hasData) {
return ListView.builder(
itemCount: 2,
itemBuilder: (BuildContext context, int index) {
// return bannerListTile(advertisements, index, context);
return const Text('index');
});
} else if (shot.hasError) {
return NewsError(
errorMessage: '${shot.hasError}',
);
} else {
return const NewsLoading(
text: 'loading...',
);
}
});
}
}
I am trying to combine two different API and fetch the results but in this structure I cannot get any data and run only ProgressBarIndicator.
If I am use regular FutureBuilder the JSON calls works without any problem. My goal is get data from two different API's like shot.data[0].value and shot.data[1].value
you made mistake in defining the getData() function.
remove getData from build method and put outside build method because the build is itself a method, you cant define a method inside a method in Dart.
class AdView extends StatefulWidget {
const AdView({Key? key, required String id}) : super(key: key);
final id = '2';
#override
_AdViewState createState() => _AdViewState();
}
class _AdViewState extends State<AdView> {
final _adService = NewsService();
late Future<Categories> _futureCategories;
late Future<AdBanner> _futureBanners;
#override
void initState() {
super.initState();
}
Future getData() async {
_futureCategories = _adService.getAllCategories();
_futureBanners = _adService.getAds('2');
AdBanner foos;
Categories bars;
await Future.wait<void>([
_futureBanners.then((result) => foos = result),
_futureCategories.then((result) => bars = result),
]);
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getData(),
builder: (BuildContext context, AsyncSnapshot<dynamic> shot) {
// (BuildContext context, AsyncSnapshot<List<dynamic>> shot) {
if (shot.hasData) {
return ListView.builder(
itemCount: 2,
itemBuilder: (BuildContext context, int index) {
// return bannerListTile(advertisements, index, context);
return const Text('index');
});
} else if (shot.hasError) {
return NewsError(
errorMessage: '${shot.hasError}',
);
} else {
return const NewsLoading(
text: 'loading...',
);
}
});
}
}