image

Flutter

24 March 2026


Great Videos

Video
Video

Set up

Dowload flutter

Download command-line tool and platform-tools and place in ../android-sdk

Use command-line tool sdkmanager to download platforms and build-tools

/opt/flutter/android-sdk
 
├── build-tools
├── cmdline-tools
|   ├── latest
├── licenses
├── platforms
└── platform-tools

flutter config --android-sdk

Install linux tool

dnf install clang cmake ninja-build gtk3-devel mesa-demos

The output confirms that you have the OpenJDK Runtime Environment (JRE) installed, but not the Java Development Kit (JDK).

The openjdk-21-jre package (which you have) provides the java command to run apps, but openjdk-21-jdk is a separate package that adds the javac command and other development tools. Build tools like Gradle specifically look for the JDK to handle the compileDebugJavaWithJavac task

Install the Compiler (JDK)

sudo apt install openjdk-21-jdk

Basic

CLI

# Init project
flutter create my_project
 
# Create APK
flutter build apk --debug # For development (hot reload support...)
flutter build apk --release # For release
 
# Install with ADB
adb install -r build/app/outputs/flutter-apk/app-debug.apk
 
# Run
flutter attach
# or
flutter run --use-application-binary=build/app/outputs/flutter-apk/app-debug.apk

Gradle

# Gradle keeps a background process (the Daemon) running even after your build finishes.
# It keep the next build faster.
./gradlew --stop
 
./gradlew clean

Use clean only when you get a weird Android error that won't go away.

Use --stop when you are done building for the day (or session) to give your Fedora OS its RAM back.

scrcpy

scrcpy -m 1024 -b 2M --max-fps 30 --no-audio
  • -m 1024: Max width
  • -b 2M: Bit rate
  • --max-fps 30: Max fps

When you ONLY need Hot Reload (No Build needed):

  • UI/Widgets (Colors, Padding, Layouts).
  • Logic (Functions, Calculations).
  • State (How data flows).

When you MUST Build again (Hot Reload won't work):

  • Plugins/Dependencies: Adding a new package to pubspec.yaml.
  • Native Code: Modifying files in the android/ or ios/ folders (like permissions in AndroidManifest.xml).
  • Assets: Adding new images or fonts.
  • App Crashes: Sometimes a "Hot Restart" (Shift + R) can't recover the app's memory, and a fresh install is cleaner.

~/.gradle/caches

What happens when remove it

  • High CPU Usage: The next build will take 5–10 minutes as it re-indexes and re-compiles everything.

  • Massive Download Time: Gradle will have to re-download every single dependency (Kotlin, Firebase, Support Libraries, etc.).

When you should remove it

  • You see errors like Could not expand ZIP... or Unexpected end of ZLIB input stream.

  • You’ve already run flutter clean and ./gradlew clean and the build still fails.

Widget lifecycle

initState(): Called only once the widget is created

build(): Called after initState(), and every we use setState()

dispose(): When the widget()/ state object is removed

Custom component

widgets/app_button.dart
class MyButton extends StatelessWidget {
 
  final Widget child;
  final Function()? onPress;
 
  const MyButton({this.child, this.onPress});
 
  @override
  Widget build(BuildContext context) {
    return  FloatingActionButton(
      onPressed: props.onPress,
      child: props.child
    );
  }
}
...
MyButton(props: ButtonProps(Text("hehe"), onPress))

Apply custom font

Add font to assets/fonts

pubspec.yaml
flutter:
  fonts:
    - family: MyCustomFont
      fonts:
        - asset: assets/fonts/MyVariableFont.ttf

Run: flutter pub get

Apply

Text(
  'Hello Variable Font',
  style: TextStyle(
    fontFamily: 'MyCustomFont',
    fontWeight: FontWeight.w700, // Vary this for weight changes
  ),
)

Apply entire app

app.dart
MaterialApp(
  theme: ThemeData(
    fontFamily: 'MyCustomFont',
  ),
  home: MyHomePage(),
);

File content structure

Variables/State Variables: Define state variables at the top.

Lifecycle Methods: initState(), dispose(), etc.

build Method: The top-level UI description.

Helper Methods/UI Functions: Small functions for rendering specific widgets (e.g., _buildHeader()).

Logic Handlers: Functions that update state (e.g., _onLoginPressed()).

Route and navigate

Built-in route generator

Basic push screen navigate

void _onTapNavigate(BuildContext context, int id) {
  Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => ProductDetail(id)),
  );
}

Route generator

route.dart
const HOME_ROUTE = '/';
 
class RouterGenerator {
  static RouteFactory routes() {
    return (settings) {
      final  agrs = settings.arguments as Map<String, dynamic>;
      Widget screen;
      switch(settings.name) {
        case HOME_ROUTE:
          screen = Home();
          break;
        default:
         screen = NotFound();
      }
 
      return MaterialPageRoute(builder: (context) => screen);
    };
  }
}
app.dart
class App extends StatelessWidget {
  const App({super.key});
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Home(),
      onGenerateRoute: RouterGenerator.routes(),
    );
  }
}
page.dart
void _onTapNavigate(BuildContext ct, int id) {
  Navigator.pushNamed(ct, PRODUCT_DETAIL_ROUTE, arguments: {'id': id});
}

MediaQuery.sizeOf(context): Rebuilds the widget only when the size property (width and height) changes.

Navigator

push(): Adds a new route to the top of the stack. Use case: Standard forward navigation, allowing the user to return to the previous screen using the back button.

  • Example: Navigator.push(context, MaterialPageRoute(builder: (context) => SecondRoute()),);.

pop(): Removes the current route from the top of the stack and returns to the previous screen. Use case: Going back to the previous screen. The system back button and the AppBar back button typically call this automatically.

  • Example: Navigator.pop(context);.

pushNamed(): Navigates to a new route using a predefined string name. Use case: Used in large apps to centralize route definitions and avoid code duplication. Requires defining a routes map or onGenerateRoute in your MaterialApp.

  • Ex: Navigator.pushNamed(ct, PRODUCT_DETAIL_ROUTE, arguments: {'id': id});

pushReplacement(): Replaces the current route on the stack with a new one.

Use case: Scenarios where the user should not be able to return to the previous screen (e.g., after a login/onboarding flow).

pushAndRemoveUntil(): Pushes a new route onto the stack and removes all previous routes until a specific condition (predicate) is met. Use case: Clearing the entire navigation history, such as after logging out, and returning to the main login or welcome screen.

popUntil(): Removes routes from the top of the stack until a given predicate returns true

Simple state management

Simple app state management

provider.dart
class ProductController extends ChangeNotifier {
  bool _isLoading = true;
  bool get isLoading => _isLoading;
 
  Future<void> getProducts() async {
    this._isLoading = true;
    notifyListeners();
    ...
  }
}

Import between page

home.dart
class _HomeState extends State<Home> {
  final _controller = ProductController();
  ...
  @override
  Widget build(BuildContext context) {
    ...
    TopSearch(onSearch:_controller.searchProducts),
    ListenableBuilder
      listenable: _controller,
      builder: (context, child) {
      // access _controller.isLoading 
      },
    ),
    ...
  }
}

Global use

main.dart
void main() {
  runApp(
    MultiProvider(
      providers: [ChangeNotifierProvider(create: (_) => ProductProvider())],
      child: const App(),
    ),
  );
}
home.dart
return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text('Total price: ${cart.totalPrice}');
  },
);

The third argument is child, which is there for optimization. If you have a large widget subtree under your Consumer that doesn't change when the model changes, you can construct it once and get it through the builder.

home.dart
return Consumer<CartModel>(
  builder: (context, cart, child) => Stack(
    children: [
      // Use SomeExpensiveWidget here, without rebuilding every time.
      ?child,
      Text('Total price: ${cart.totalPrice}'),
    ],
  ),
  // Build the expensive widget here.
  child: const SomeExpensiveWidget(),
);
Put your Consumer widgets as deep in the tree as possible.

The easiest way to read a value is by using the extension methods on [BuildContext]:

  • context.watch<T>(), which makes the widget listen to changes on T
  • context.read<T>(), which returns T without listening to it
  • context.select<T, R>(R cb(T value)), which allows a widget to listen to only a small part of T.

One can also use the static method Provider.of<T>(context), which will behave similarly to watch. When the listen parameter is set to false (as in Provider.of<T>(context, listen: false)), then it will behave similarly to read.

It's worth noting that context.read<T>() won't make a widget rebuild when the value changes and it cannot be called inside StatelessWidget.build/State.build. On the other hand, it can be freely called outside of these methods.

Theme

See more

Flutter applies styling in the following order:

  • Styles applied to the specific widget.
  • Themes that override the immediate parent theme.
  • Main theme for the entire app.

After you define a Theme, Flutter's Material widgets use your theme to set the background colors and font styles for app bars, buttons, checkboxes, and more.

themeData(
  useMaterial3: true,
  colorScheme: ColorScheme.light(
    onSurface: AppColors.textStrongLight,
    brightness: Brightness.light,
),
  1. The Brand (Action Colors)
PropertyPurposeWebsite Equivalent
primaryThe main brand color for high-emphasis UI.Main Buttons (Submit, Login).
onPrimaryColor for text/icons sitting on primary.Text inside
primaryContainerStand out more than the background but less than primary.Selected list items, Hover states, active Tab BG.
onPrimaryContainerColor for text/iconsText inside.
  1. Accent & Contrast (Variety)
PropertyPurposeWebsite Equivalent
secondaryAfter primaryFilter chips, toggle switches, or "Cancel" buttons.
onSecondaryColor inside.Text inside a secondary chip.
tertiaryUsed for contrasting accents or "neutralizing".Highlighting a "New" badge or a "Sale" tag.
onTertiaryColor inside.Text inside that "Sale" tag.
  1. Surface & Hierarchy (Backgrounds)
PropertyPurposeWebsite Equivalent
surfaceScreen bg.Page bg.
onSurface.The body text.
onSurfaceVariant.Description Text, Helper text, Hint text.
surfaceContainerBackground for cards.The white background of a product card.
surfaceContainerLowLow.Sidebars, Footers, or subtle backgrounds.
surfaceContainerHighestHighest.Input Field Backgrounds, Search Bars, Modal Dialogs.
outlineUsed for borders and dividers.Input field borders or horizontal lines.
surfaceTintA color overlay used to show elevation.Usually matches primary.
  1. Feedback (Status)
PropertyPurposeWebsite Equivalent
errorFor invalid states or destructive actions."Delete" buttons or error messages.
onErrorColor for text/icons sitting on error.White text on a red error banner.
errorContainerBackground for error messages.The light red background of an alert box.
onErrorContainerText color for errorContainer.Dark red text on a light red box.
return ColorScheme(
  brightness: brightness,
  primary: AppColors.primary,
  onPrimary: AppColors.onPrimary,
  secondary: AppColors.secondary,
  onSecondary: AppColors.onSecondary,
  surface: brightness == Brightness.light
      ? AppColors.lightSurface
      : AppColors.darkSurface
  ...
)

Theme tip

Defaul Text() style is bodyMedium

extension TailwindTypography on TextStyle {
  TextStyle get bold => copyWith(fontWeight: FontWeight.w700);
 
  TextStyle get textLg => copyWith(fontSize: 18, height: 1.555);
}
 
extension ThemeContext on BuildContext {
  // Shortcut to the full theme
  ThemeData get theme => Theme.of(this);
 
  // Shortcut to the color scheme
  ColorScheme get colorScheme => theme.colorScheme;
 
  // Use bodyMedium as the base
  TextStyle get textLg => theme.textTheme.bodyMedium!.textLg;
}
 

Use in app

Text(
  item.role,
  style: context.textLg.bold.copyWith(
    color: context.colorScheme.primary,
  ),
)

Localization

Video

Add 'l10n.yaml'

arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations

Enable Automatic Generation

flutter:
  generate: true

Or manual generation

flutter gen-l10n

Riverpod

Base flutter project 2026

ProviderScope

// app.dart
void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

Providers

Provider

Used for values or objects that don't change

Expose a read-only value.

final dioProvider = Provider<Dio>((ref) {
  return Dio(BaseOptions(baseUrl: 'https://api.example.com'));
});
 
// Usage: ref.read(dioProvider).get();

StateProvider (Legacy)

Can read/ write value.

final themeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.light);
 
// Usage: ref.read(themeProvider.notifier).state = ThemeMode.dark;

FutureProvider

Handles the three states of an asynchronous request: Data, Loading, and Error.

Expose a read-only value.

final userProfileProvider = FutureProvider<Map<String, dynamic>>((ref) async {
  final dio = ref.watch(dioProvider);
  final response = await dio.get('/user/info');
  return response.data;
});

NotifierProvider

In Riverpod, the Notifier is specifically designed to be the Controller. It is the single place where:

  • State is updated (Loading -> Success/Error).
  • Side effects happen (Calling the API).
  • Errors are handled (The try/catch).
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tech_review/screens/login/logic/login_state.dart';
 
 
class AuthRepository {
  Future<User> signIn(String email, String password) async {
    // Actual API implementation here
  }
}
 
class LoginNotifier extends Notifier<LoginState> {
  @override
  LoginState build() => LoginState(); // Initial state
 
  Future<void> login(String email, String password) async {
    // Set loading to true while "calling" API
    state = state.copyWith(isFetching: true, errorMessage: null);
    try {
      // Notifier calls the Repository
      final user = await ref.read(authRepositoryProvider).signIn(email, password);
      state = state.copyWith(isLoading: false, isLoggedIn: true);
    } catch (e) {
      state = state.copyWith(isLoading: false, errorMessage: e.toString());
    }
  }
 
  void logout() {
    state = LoginState(); // Reset state
  }
}
 
// Global provider instance
final loginProvider = NotifierProvider<LoginNotifier, LoginState>(() {
  return LoginNotifier();
});

AsyncNotifierProvider

Method can be return type of Future

class ProductListNotifier extends AsyncNotifier<List<String>> {
  @override
  FutureOr<List<String>> build() async {
    // 1. You can access other providers here using 'ref'
    // 2. Perform your initial API call
    return _fetchProducts();
  }
 
  // Private method to simulate an API call
  Future<List<String>> _fetchProducts() async {
    await Future.delayed(const Duration(seconds: 2)); // Simulate network lag
    return ['Laptop', 'Smartphone', 'Monitor'];
  }
}
 
 
final productListProvider = AsyncNotifierProvider<ProductListNotifier, List<String>>(() {
  return ProductListNotifier();
});

Ref

The Ref is the "bridge" that allows providers and widgets to interact with other providers.

  • WidgetRef: Used inside widgets to read or watch providers.
  • ProviderRef: Used inside a provider to access other providers.

WidgetRef

ProviderRef

Reading State

There are two main ussage:

  • ref.watch(provider): Used inside the build method. It observes the provider and triggers a rebuild of the widget whenever the state changes.

  • ref.read(provider): Used inside callbacks (like onPressed). It gets the current value of a provider once without tracking future changes. Never use this inside the build method. 1

Modifiers

To change how a provider behaves:

  • .autoDispose: Automatically destroys the state of a provider when it is no longer being used (e.g., the user leaves the screen). This is excellent for saving memory.

  • .family: Allows you to pass external parameters to a provider (e.g., fetching a specific user by an ID).

LayerRoleLogic
Widget (View)UI / User InputCalls notifier.login()
Notifier (Controller)Application Logictry/catch, updates isLoading, calls Repository
Repository (Data)Data Sourcehttp.post, jsonDecode, throws Errors

Define a state

login_state.dart
class LoginState {
  final bool isFetching;
  final String? errorMessage;
 
  const LoginState({
    this.isFetching = false,
    this.errorMessage,
  });
 
  LoginState copyWith({
    bool? isFetching,
    String? errorMessage,
  }) {
    return LoginState(
      isFetching: isFetching ?? this.isFetching,
      errorMessage: errorMessage,
    );
  }
}

Riverpod annotaion

Functional Providers

Using Provider, FutureProvider, so they are read-Only

Class-Based Providers (Managed State)

Using Notifier or AsyncNotifier

Has state and expose public methods to change that state.

TypeFunctional
(Can't perform side-effects)
Class-Based
(Can perform side-effects)
Sync
@riverpod
String example(Ref ref) {
  return 'foo'; 
}
@riverpod 
class Example extends _$Example { 
  @override String build() {
    return 'foo'; 
  } 
 
  //Add methods to mutate the state
}
Async - Future
@riverpod
Future<String> example(Ref ref) async {
  return Future.value('foo');
}                    
@riverpod
class Example extends _$Example {
  @override
  Future<String> build() async {
    return Future.value('foo');
  }
 
  // Add methods to mutate the state
}
Async - Stream
@riverpod
Stream<String> example(Ref ref) async* {
  yield 'foo';
}
@riverpod
class Example extends _$Example {
  @override
  Stream<String> build() async* {
    yield 'foo';
  }
 
  // Add methods to mutate the state
}

Riverpod Lifecycle

Go Router

app_go_router.dart
enum AppRoute { login, root, home, splash, profile, productDetail }
 
final goRouterProvider = Provider<GoRouter>((ref) {
  final router = GoRouter(
    initialLocation: '/splash',
    redirect: {}
    routes: [
      GoRoute(
        path: '/splash',
        name: AppRoute.splash.name,
        builder: (context, state) => SplashScreen(),
      ),
      GoRoute(
        path: '/home',
        name: AppRoute.home.name,
        builder: (context, state) => HomeScreen(),
      ),
    ],
  );
 
  ref.listen(authProvider, (_, __) => router.refresh());
 
  return router;
});
app.dart
class App extends ConsumerWidget {
  const App({super.key});
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final router = ref.read(goRouterProvider);
 
    return MaterialApp.router(
      routerConfig: router,
      theme: AppThemes.lightTheme,
    );
  }
}

Usage

context.pushNamed(
  AppRoute.productDetail.name,
  pathParameters: {'id': id.toString()},
);

Code generation

Example model define with auto generate immutable classes and from/ to json logic.

models/tag_model.dart
part 'tag.freezed.dart';
part 'tag.g.dart';
 
Tag tagFromJson(String str) => Tag.fromJson(json.decode(str));
String tagToJson(Tag data) => json.encode(data.toJson());
 
@freezed // From freezed_annotation
class Tag with _$Tag {
    const factory Tag({
        @JsonKey(name: "id")
        required int id,
        @JsonKey(name: "category_id") // From json_annotation
        required int categoryId,
        @JsonKey(name: "name")
        required String name,
    }) = _Tag;
 
    factory Tag.fromJson(Map<String, dynamic> json) => _$TagFromJson(json);
}

Break thought:

  • build_runner: The tool that runs the generation process.
  • freezed: Creates immutable classes, copyWith, etc.
  • json_serializable: Creates fromJson and toJson logic.
  • json_annotation: Correct naming conventions, dart use camelCase, api use snake_case.

Build_runner

ActionSpeed (after start)Terminate?
watchReacts to file savesFast (1-3s)No (stays open)
buildRuns onceSlow (30s+)Yes
cleanDeletes build cacheInstantYes

Delete generated files

find . -name "*.g.dart" -type f -delete

Add to ~/.bashrc

alias brb="dart run build_runner build --delete-conflicting-outputs"
alias brw="dart run build_runner watch --delete-conflicting-outputs"

Widgets

Basic

Row doesn’t constrain children width, so the button not know how wide it should be

Inside a Row, always constrain width using:

  • Expanded: Same as flex:1 (1 1 0), behaves like a "proportion" setting. All items with flex: 1 take up equal width/height, regardless of content size, because they start from a basis of 0.

  • Flexible: Same as flex:auto (1 1 auto), behaves like a "size-to-content" setting. Items grow and shrink to fill the space, but their initial size is based on their content

  • SizedBox: Constraints with specific size

Animation

Codebase animation

Implicit Animations: The simplest handles the transition between old and new property values.

  • Pre-packaged Widgets: Ready-to-use widgets named AnimatedFoo (e.g., AnimatedContainer, AnimatedOpacity, AnimatedPositioned).

  • Custom Implicit: TweenAnimationBuilder allows you to implicitly animate properties for which a pre-built widget doesn't exist.

tween_animation_builder_example.dart
TweenAnimationBuilder<double>(
  tween: Tween<double>(begin: 0, end: 1),
  duration: const Duration(seconds: 2),
  curve: Curves.linear,
  builder: (BuildContext context, double val, Widget? child) {
    return Opacity(
      opacity: val,
      child: Container(
        width: 200,
        height: 100,
        color: Colors.blue,
        child: child, // The static child passed below
      ),
    );
  },
  child: const Center(
    child: Text(
      'TweenAnimationBuilder!',
      style: TextStyle(color: Colors.white),
    ),
  ),
),
fade_animation_example.dart
class LogoFadeAnimationState extends State<LogoFadeAnimation> {
  bool _isShow = false;
 
  void _changeOpacity() {
    setState(() => _isShow = !_isShow);
  }
 
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedOpacity(
          curve: Curves.easeIn,
          opacity: _isShow ? 1 : 0,
          duration: const Duration(seconds: 1),
          child: FlutterLogo(size: 100),
        ),
 
        ElevatedButton(
          onPressed: _changeOpacity,
          child: Text(_isShow ? 'hide' : 'show'),
        ),
      ],
    );
  }
}

Explicit Animations: More control, coordinating multiple animations. These require an AnimationController.

  • Transition Widgets: Built-in widgets named FooTransition (e.g., FadeTransition, ScaleTransition) that take an explicit Animation object.

  • Custom Explicit: Using AnimatedBuilder or subclassing AnimatedWidget for complex, custom logic

scale_transition_example.dart
class _ScaleTransitionExampleState extends State<ScaleTransitionExample>
    with TickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(
    duration: const Duration(milliseconds: 350),
    vsync: this,
  );
 
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
 
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ScaleTransition(
          scale: _controller,
          child: const Padding(
            padding: EdgeInsets.all(8.0),
            child: FlutterLogo(size: 150.0),
          ),
        ),
 
        ElevatedButton(
          child: Text("Toggle"),
          onPressed: () {
            _controller.isCompleted
                ? _controller.reverse()
                : _controller.forward();
          },
        ),
      ],
    );
  }
}

use with Tween

scale_transition_example.dart
late final Animation<double> _animation = Tween<double>(
  begin: 0.8,
  end: 1.0,
).animate(_controller);
// ).animate(CurvedAnimation(parent: _controller, curve: Curves.fastOutSlowIn));
 
...
ScaleTransition(
  scale: _animation,
  child: const Padding(
    padding: EdgeInsets.all(8.0),
    child: FlutterLogo(size: 150.0),
  ),
),
animated_builder_example.dart
class _AnimatedBuilderExampleState extends State<AnimatedBuilderExample>
    with TickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(
    duration: const Duration(seconds: 5),
    vsync: this,
  )..repeat(reverse: true);
 
  late final Animation<double> _scale = Tween<double>(
    begin: 1,
    end: 0.5,
  ).animate(_controller);
 
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
 
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      child: Container(
        width: 200.0,
        height: 200.0,
        color: Colors.blue,
        child: const Center(child: Text('Whee!')),
      ),
      builder: (BuildContext context, Widget? child) {
        return Transform.rotate(
          angle: _controller.value * 2 * math.pi,
          child: Transform.scale(scale: _scale.value, child: child),
        );
      },
    );
  }
}

Make withby Nguyen Huu Dat

© 2025