
24 March 2026
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-toolsflutter config --android-sdk
Install linux tool
dnf install clang cmake ninja-build gtk3-devel mesa-demosThe 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# 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.apkGradle
# Gradle keeps a background process (the Daemon) running even after your build finishes.
# It keep the next build faster.
./gradlew --stop
./gradlew cleanUse 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 fpsWhen you ONLY need Hot Reload (No Build needed):
When you MUST Build again (Hot Reload won't work):
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.
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
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))Add font to assets/fonts
flutter:
fonts:
- family: MyCustomFont
fonts:
- asset: assets/fonts/MyVariableFont.ttfRun: flutter pub get
Apply
Text(
'Hello Variable Font',
style: TextStyle(
fontFamily: 'MyCustomFont',
fontWeight: FontWeight.w700, // Vary this for weight changes
),
)Apply entire app
MaterialApp(
theme: ThemeData(
fontFamily: 'MyCustomFont',
),
home: MyHomePage(),
);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()).
Built-in route generator
Basic push screen navigate
void _onTapNavigate(BuildContext context, int id) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ProductDetail(id)),
);
}Route generator
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);
};
}
}class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
onGenerateRoute: RouterGenerator.routes(),
);
}
}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.
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.
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.
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
class ProductController extends ChangeNotifier {
bool _isLoading = true;
bool get isLoading => _isLoading;
Future<void> getProducts() async {
this._isLoading = true;
notifyListeners();
...
}
}Import between page
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
void main() {
runApp(
MultiProvider(
providers: [ChangeNotifierProvider(create: (_) => ProductProvider())],
child: const App(),
),
);
}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.
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(),
);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 Tcontext.read<T>(), which returns T without listening to itcontext.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.
Flutter applies styling in the following order:
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,
),| Property | Purpose | Website Equivalent |
|---|---|---|
primary | The main brand color for high-emphasis UI. | Main Buttons (Submit, Login). |
onPrimary | Color for text/icons sitting on primary. | Text inside |
primaryContainer | Stand out more than the background but less than primary. | Selected list items, Hover states, active Tab BG. |
onPrimaryContainer | Color for text/icons | Text inside. |
| Property | Purpose | Website Equivalent |
|---|---|---|
secondary | After primary | Filter chips, toggle switches, or "Cancel" buttons. |
onSecondary | Color inside. | Text inside a secondary chip. |
tertiary | Used for contrasting accents or "neutralizing". | Highlighting a "New" badge or a "Sale" tag. |
onTertiary | Color inside. | Text inside that "Sale" tag. |
| Property | Purpose | Website Equivalent |
|---|---|---|
surface | Screen bg. | Page bg. |
onSurface | . | The body text. |
onSurfaceVariant | . | Description Text, Helper text, Hint text. |
surfaceContainer | Background for cards. | The white background of a product card. |
surfaceContainerLow | Low. | Sidebars, Footers, or subtle backgrounds. |
surfaceContainerHighest | Highest. | Input Field Backgrounds, Search Bars, Modal Dialogs. |
outline | Used for borders and dividers. | Input field borders or horizontal lines. |
surfaceTint | A color overlay used to show elevation. | Usually matches primary. |
| Property | Purpose | Website Equivalent |
|---|---|---|
error | For invalid states or destructive actions. | "Delete" buttons or error messages. |
onError | Color for text/icons sitting on error. | White text on a red error banner. |
errorContainer | Background for error messages. | The light red background of an alert box. |
onErrorContainer | Text 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,
),
)Add 'l10n.yaml'
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizationsEnable Automatic Generation
flutter:
generate: trueOr manual generation
flutter gen-l10nBase flutter project 2026
// app.dart
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}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:
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();
});The Ref is the "bridge" that allows providers and widgets to interact with other providers.
WidgetRef
ProviderRef
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
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).
| Layer | Role | Logic |
|---|---|---|
| Widget (View) | UI / User Input | Calls notifier.login() |
| Notifier (Controller) | Application Logic | try/catch, updates isLoading, calls Repository |
| Repository (Data) | Data Source | http.post, jsonDecode, throws Errors |
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,
);
}
}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.
| Type | Functional (Can't perform side-effects) | Class-Based (Can perform side-effects) |
|---|---|---|
| Sync | | |
| Async - Future | | |
| Async - Stream | | |
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;
});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()},
);Example model define with auto generate immutable classes and from/ to json logic.
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.| Action | Speed (after start) | Terminate? | |
|---|---|---|---|
| watch | Reacts to file saves | Fast (1-3s) | No (stays open) |
| build | Runs once | Slow (30s+) | Yes |
| clean | Deletes build cache | Instant | Yes |
Delete generated files
find . -name "*.g.dart" -type f -deleteAdd to ~/.bashrc
alias brb="dart run build_runner build --delete-conflicting-outputs"
alias brw="dart run build_runner watch --delete-conflicting-outputs"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
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.
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),
),
),
),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
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
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),
),
),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 with
by Nguyen Huu Dat
© 2025