
10 April 2026
The "Pro" Workflow for Laptops
Develop on Linux Desktop (-d linux): Use this for 90% of your work (styling, navigation, logic). It’s the fastest way to iterate.
Resize the Window: Manually shrink your Linux app window to a "phone shape" to check your layout.
3.Final Check on Phone (-d android): Once a feature is done, run it on your physical phone via USB/Scrcpy to test touch feel and performance.z
Keep widget single responsibility, separate ui from logic
Keep widget short, 200 lines of code is the best
snake_case naming
Follow flutter flutter repository
Folder structure, DDD
Start your code with ci/cd in mind
Not hard code style in to widget
Use const when ever possible
Overlay method
Stack(
children: [
if (!appLoading) _pageContent(homeState),
// The loading overlay
if (appLoading)
Container(
color: Colors.white,
child: const Center(child: CircularProgressIndicator()),
),
],
),AppAsyncValueView
// app_async_value_view.dart
final AsyncValue<T> value;
final Widget Function(T data) data;
final VoidCallback? onRetry;
@override
Widget build(BuildContext context) {
return value.when(
loading: () => Center(child: Text('Loading')),
error: (error, stackTrace) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Error"),
const SizedBox(height: 12),
if (onRetry != null)
OutlinedButton(onPressed: onRetry, child: Text("Retry")),
],
),
),
data: data,
);
}// home_state.dart
@freezed
abstract class HomeState with _$HomeState {
const factory HomeState({
// @Default([]) List<ProductList> products,
@Default(AsyncValue.loading()) AsyncValue<List<ProductList>> products,
@Default([]) List<ProductList> searchResults,
@Default(false) bool isSearch,
@Default(false) bool isLoading,
}) = _HomeState;
}// home_controller.dart
@riverpod
class HomeController extends _$HomeController {
late final Dio _dio;
@override
HomeState build() {
_dio = ref.watch(dioProvider);
return HomeState();
}
Future<List<ProductList>> callGetProducts() async {
final res = await _dio.get('/products?page=1');
final GetProductsResponse payload = GetProductsResponse.fromJson(res.data);
return payload.products;
}
Future<void> fetchProducts() async {
state = state.copyWith(products: const AsyncValue.loading());
state = state.copyWith(
products: await AsyncValue.guard(() async {
return await callGetProducts();
}),
);
}
}// home_screen.dartfinal routerProvider = Provider<GoRouter>((ref) {
final router = GoRouter(
initialLocation: '/',
redirect: (context, state) {
final auth = ref.read(authProvider);
...
},
);
ref.listen(authProvider, (_, __) => router.refresh());
return router;
});Outside Redirect: Variables are static. They are evaluated once when the provider initializes.
Inside Redirect: Logic is dynamic. It is evaluated every time the router checks the path.
class ProductDetailScreen extends ConsumerStatefulWidget {
final String id;
}
class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
late String id;
@override
void initState() {
super.initState();
id = widget.id;
}
}void initState() {
super.initState();
Future.microtask(() => ref.read(authProvider.notifier).init());
}HomeState build() {
Future.microtask(() async {
await fetchMetadata();
});
return const HomeState();
} Make with
by Nguyen Huu Dat
© 2025