FetchRestaurants Class in FlutterThe FetchRestaurants class is a state model used to represent the result of fetching restaurant data from an API in a Flutter application. Instead of returning only a list of restaurants, this class wraps everything the UI needs to properly react to the API request, such as loading state, errors, pagination information, and retry logic. This approach keeps the UI clean and predictable.
This class does not perform the network request itself. Its only responsibility is to store the result and status of the request. In real applications, the actual API call usually happens in a service, provider, controller, or view model. Once the request finishes, an instance of FetchRestaurants is created and passed to the UI.
class FetchRestaurants {
final List<Restaurant>? restaurants;
final int currentPage;
final int totalPages;
final ApiError? error;
final bool isLoading;
final Function? refetch;
FetchRestaurants(
{required this.restaurants,
required this.currentPage,
required this.totalPages,
required this.error,
required this.isLoading,
required this.refetch});
}
This function, fetchRestaurants, is a custom Flutter Hook that fetches restaurant data from a backend API and returns a structured result using the FetchRestaurants model. It is designed to be used inside a HookWidget, allowing the UI to automatically react to loading, success, and error states without manually managing lifecycle methods.
Rather than returning raw API data, this function returns a complete fetch state, including the restaurant list, pagination data, loading status, error information, and a way to refetch the data.
Inside the function, useState is used to manage local state such as restaurants, isLoading, currentPage, totalPages, and error. Flutter Hooks allow this logic to exist outside of a traditional StatefulWidget, making the code more concise and reusable.
Each call to useState creates a reactive value. When the value changes, Flutter automatically rebuilds the UI that depends on it. This makes hooks ideal for API fetching logic.
The function uses Get.put(PaginationController()) to retrieve or create a pagination controller using GetX. This controller tracks the current page number for different restaurant statuses such as Pending, Verified, and Rejected. Depending on the selected status, the correct page value is used to build the API request URL.
This separation allows pagination logic to live in a controller, keeping the data-fetching function focused only on networking and state updates.
Inside the fetchData function, the API URL is constructed dynamically based on the restaurant status. Each status maps to a different pagination value stored in the controller. This makes it possible to maintain separate page counters for different restaurant categories without mixing their state.
Once the URL is created, an HTTP GET request is sent using the http package.
If the server responds with a 200 status code, the JSON response is decoded and converted into a Restaurants model. The restaurant list, current page, and total pages are then extracted and stored in their respective state variables. The total page count is also passed back to the pagination controller so it can manage pagination limits.
If the response status code is not successful, the error response is parsed into an ApiError object, allowing the UI to display meaningful error messages instead of failing silently.
The isLoading flag is set to true before the request starts and reset to false once the request finishes, regardless of success or failure. This ensures that loading indicators in the UI are accurate and consistent.
By managing loading state explicitly, the UI can clearly distinguish between “loading”, “loaded”, and “error” states.
useEffectThe useEffect hook ensures that fetchData() runs automatically when the widget is first built. This mimics the behavior of initState() in a traditional StatefulWidget. The empty dependency array means the data is fetched only once unless the widget is rebuilt from scratch.
The refetch function allows the UI to manually trigger the API call again. This is useful for pull-to-refresh actions or retry buttons when a request fails. By exposing refetch, the data-fetching logic stays reusable and clean.
At the end of the function, a FetchRestaurants object is returned. This object bundles together the restaurant data, loading state, pagination information, error details, and refetch logic into a single, easy-to-consume structure.
This design keeps UI code simple. Instead of checking multiple variables scattered across a widget, the UI only needs to react to one object that represents the full API state.
This hook-based approach promotes clean architecture by separating data fetching, state management, and UI rendering. It scales well for large applications and works seamlessly with Flutter Hooks and GetX. For blog readers, this pattern is an excellent introduction to building maintainable, production-ready API layers in Flutter.
FetchRestaurants fetchRestaurants(
String status,
int page,
int limit,
) {
final restaurants = useState<List<Restaurant>?>(null);
final isLoading = useState<bool>(false);
final currentPage = useState<int>(1);
final totalPages = useState<int>(0);
final error = useState<ApiError?>(null);
final appiError = useState<ApiError?>(null);
final controller = Get.put(PaginationController());
Future<void> fetchData() async {
isLoading.value = true;
try {
Uri url = Uri.parse('');
if (status == "Pending") {
url = Uri.parse(
'$appBaseUrl/api/admin/restaurants?page=${controller.pendingRestaurants}&status=$status');
} else if (status == "Verified") {
url = Uri.parse(
'$appBaseUrl/api/admin/restaurants?page=${controller.verifiedRestaurants}&status=$status');
} else if (status == "Rejected") {
url = Uri.parse(
'$appBaseUrl/api/admin/restaurants?page=${controller.rejectedRestaurants}&status=$status');
}
final response = await http.get(url);
if (response.statusCode == 200) {
var data = jsonDecode(response.body);
restaurants.value = Restaurants.fromJson(data).restaurants;
currentPage.value = Restaurants.fromJson(data).currentPage;
totalPages.value = Restaurants.fromJson(data).totalPages;
controller.updateTotalPages = Restaurants.fromJson(data).totalPages;
} else {
appiError.value = apiErrorFromJson(response.body);
}
} catch (e) {
debugPrint(e.toString());
} finally {
isLoading.value = false;
}
}
useEffect(() {
fetchData();
return null;
}, []);
void refetch() {
isLoading.value = true;
fetchData();
}
return FetchRestaurants(
restaurants: restaurants.value,
isLoading: isLoading.value,
error: error.value,
currentPage: currentPage.value,
totalPages: totalPages.value,
refetch: refetch,
);
}
PendingRestaurantsThe PendingRestaurants widget is responsible for displaying a list of restaurants that are in the “Pending” state. It is built as a HookWidget, which allows it to use Flutter Hooks for stateful logic without needing a traditional StatefulWidget. This keeps the UI code simple while still supporting dynamic data fetching and pagination.
Inside the build method, two controllers are initialized using GetX: StatusController and PaginationController. The StatusController is used to store a reference to the refetch function so that other parts of the app can trigger a reload of restaurant data. The PaginationController keeps track of the current page for pending restaurants and helps manage pagination state across the app.
This approach allows pagination and refresh logic to be shared and reused instead of being tightly coupled to a single screen.
The line fetchRestaurants("Pending", 1, 5) calls a custom hook that fetches restaurant data from the backend. This hook returns a FetchRestaurants object that contains everything the UI needs, including the restaurant list, loading state, pagination data, error information, and a refetch function.
By destructuring this result into local variables, the widget gains access to all relevant fetch states in a clean and readable way.
Before rendering the main UI, the widget checks several conditions. If the data is still loading, a shimmer effect is displayed using RestaurantTileShimmer. This provides visual feedback to the user that content is being loaded.
If the request completes but returns an empty list, a simple message is shown indicating that no restaurants were found. If an error occurs during the request, an error message is displayed instead. Handling these states explicitly ensures that the UI remains predictable and user-friendly.
When data is successfully loaded, the widget returns a WrapperWidget. This widget acts as a container for pagination logic and layout structure. It receives the current page, total number of pages, and the refetch function so it can handle page changes.
The restaurant list is rendered using List.generate, which creates a RestaurantTile widget for each restaurant in the list. Each tile is responsible for displaying individual restaurant details, keeping the UI modular and reusable.
Pagination is handled through the onPageChanged callback. When the page changes, the pendingRestaurants page value in the pagination controller is updated, and the refetch function is called to load data for the new page. This creates a smooth pagination experience without tightly coupling pagination logic to the UI.
This widget follows a clean separation of concerns. Data fetching is handled by a custom hook, pagination state lives in a controller, and UI rendering is kept focused and readable. By combining Flutter Hooks with GetX, the code remains scalable and easy to maintain, making it suitable for real-world admin dashboards and production-ready Flutter applications.
class PendingRestaurants extends HookWidget {
const PendingRestaurants({super.key});
@override
Widget build(BuildContext context) {
final controller = Get.put(StatusController());
final pagination = Get.put(PaginationController());
final data = fetchRestaurants("Pending", 1, 5);
final restaurants = data.restaurants;
final isLoading = data.isLoading;
final error = data.error;
final refetch = data.refetch;
final totalPages = data.totalPages;
final currentPage = data.currentPage;
controller.setData(data.refetch!);
if (isLoading) {
return const RestaurantTileShimmer();
}
if (restaurants!.isEmpty) {
return const Center(
child: Text("No restaurants found"),
);
}
if (error != null) {
return const Center(
child: Text("An error occured"),
);
}
return WrapperWidget(
currentPage: currentPage,
refetch: refetch,
totalPages: totalPages,
onPageChanged: (int value) {
pagination.pendingRestaurants = value + 1;
refetch!();
},
children: List.generate(restaurants.length, (index) {
final restaurant = restaurants[index];
return RestaurantTile(restaurant: restaurant);
}));
}
}
Restaurants Data ModelThis file defines the data models used to convert restaurant data from a backend API into usable Dart objects in a Flutter application. When an API returns JSON data, Flutter cannot work with it directly. These models act as a bridge between raw JSON and strongly typed Dart classes, making the app safer and easier to maintain.
The restaurantsFromJson function is a small helper that takes a JSON string and converts it into a Restaurants object. It uses Dart’s json.decode method and passes the result to the model’s fromJson factory constructor. This allows API responses to be parsed with a single function call.
Restaurants Wrapper ModelThe Restaurants class represents the entire API response, not just a single restaurant. It contains a list of restaurant objects along with pagination information such as the current page and the total number of pages. This structure is common in paginated APIs where data is returned in chunks rather than all at once.
By keeping pagination data and the restaurant list together, the UI can easily manage page navigation without relying on separate variables or hardcoded values.
Restaurant ModelThe Restaurant class represents a single restaurant entity. Each field maps directly to a value returned by the API, such as the restaurant’s name, availability status, logo URL, rating, and estimated time. Using required fields ensures that the app always receives the data it expects, reducing the chance of runtime errors.
The fromJson factory constructor handles the conversion from raw JSON into a strongly typed Dart object. This makes it easier to work with restaurant data throughout the app using properties instead of map keys.
CoordsSome API responses include nested objects. In this case, the restaurant’s location information is stored inside a coords object. The Coords class is used to parse and store this nested data cleanly, instead of mixing location fields directly into the restaurant model.
This approach keeps the code organized and mirrors the structure of the backend response, making it easier to understand and extend later.
import 'dart:convert';
Restaurants restaurantsFromJson(String str) => Restaurants.fromJson(json.decode(str));
class Restaurants {
final List<Restaurant> restaurants;
final int currentPage;
final int totalPages;
Restaurants({
required this.restaurants,
required this.currentPage,
required this.totalPages,
});
factory Restaurants.fromJson(Map<String, dynamic> json) => Restaurants(
restaurants: List<Restaurant>.from(json["restaurants"].map((x) => Restaurant.fromJson(x))),
currentPage: json["currentPage"],
totalPages: json["totalPages"],
);
}
class Restaurant {
final Coords coords;
final String id;
final String title;
final String time;
final bool isAvailable;
final String logoUrl;
final double rating;
final String ratingCount;
Restaurant({
required this.coords,
required this.id,
required this.title,
required this.time,
required this.isAvailable,
required this.logoUrl,
required this.rating,
required this.ratingCount,
});
factory Restaurant.fromJson(Map<String, dynamic> json) => Restaurant(
coords: Coords.fromJson(json["coords"]),
id: json["_id"],
title: json["title"],
time: json["time"],
isAvailable: json["isAvailable"],
logoUrl: json["logoUrl"],
rating: json["rating"].toDouble(),
ratingCount: json["ratingCount"],
);
}
class Coords {
final String address;
Coords({
required this.address,
});
factory Coords.fromJson(Map<String, dynamic> json) => Coords(
address: json["address"],
);
}