image

Flutter Practice

10 April 2026


Linux native support

The "Pro" Workflow for Laptops

  1. Develop on Linux Desktop (-d linux): Use this for 90% of your work (styling, navigation, logic). It’s the fastest way to iterate.

  2. 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

Best practices

  1. Keep widget single responsibility, separate ui from logic

  2. Keep widget short, 200 lines of code is the best

  3. snake_case naming

  4. Follow flutter flutter repository

  5. Folder structure, DDD

  6. Start your code with ci/cd in mind

  7. Not hard code style in to widget

  8. Use const when ever possible

Practive

Handle loading state

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.dart

Riverpod in go_router

final 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.

Detail page

class ProductDetailScreen extends ConsumerStatefulWidget {
  final String id;
}
 
class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
  late String id;
 
    @override
  void initState() {
    super.initState();
 
    id = widget.id;
  }
}

Init fetch data

page.dart
void initState() {
  super.initState();
  Future.microtask(() => ref.read(authProvider.notifier).init());
}
home_provider.dart
HomeState build() {
  Future.microtask(() async {
    await fetchMetadata();
  });
 
  return const HomeState();
}
 

Make withby Nguyen Huu Dat

© 2025