Flutter cho người mới: xây ứng dụng ghi chú công việc đầu tiên

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Flutter cho phép bạn viết một lần và chạy trên Android, iOS, web và máy tính. Bài này hướng dẫn bạn tạo ứng dụng ghi chú công việc đầu tiên, giúp hiểu ba khái niệm lõi: widget, trạng thái và danh sách cuộn.

Chuẩn bị

Cài Flutter SDK theo hướng dẫn chính thức cho hệ điều hành của bạn, cài Android Studio hoặc VS Code kèm plugin Flutter, rồi chạy lệnh kiểm tra sức khỏe cài đặt:

flutter doctor

Lệnh này liệt kê những gì còn thiếu (Android SDK, giấy phép, thiết bị). Hãy xử lý lần lượt các mục báo lỗi. Sau đó tạo dự án:

flutter create todo_app
cd todo_app
flutter run

Bạn có thể chạy trên máy ảo Android, điện thoại thật cắm cáp, hoặc chọn thiết bị Chrome để chạy bản web cho nhanh.

Tư duy Flutter: mọi thứ là widget

Trong Flutter, nút bấm, văn bản, khoảng cách, bố cục, thậm chí cả ứng dụng đều là widget, và widget được lồng vào nhau thành cây. Có hai loại chính. StatelessWidget không có trạng thái thay đổi. StatefulWidget có trạng thái, và khi bạn gọi setState thì giao diện được dựng lại với dữ liệu mới.

Bước 1: Dựng khung ứng dụng

Mở lib/main.dart, xóa toàn bộ nội dung mặc định và viết:

import 'package:flutter/material.dart';

void main() => runApp(const TodoApp());

class TodoApp extends StatelessWidget {
  const TodoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Việc cần làm',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: const TodoPage(),
    );
  }
}

Bước 2: Trang danh sách có trạng thái

class Todo {
  Todo(this.title, {this.done = false});
  final String title;
  bool done;
}

class TodoPage extends StatefulWidget {
  const TodoPage({super.key});

  @override
  State<TodoPage> createState() => _TodoPageState();
}

class _TodoPageState extends State<TodoPage> {
  final List<Todo> _todos = [];
  final TextEditingController _controller = TextEditingController();

  void _addTodo() {
    final text = _controller.text.trim();
    if (text.isEmpty) return;
    setState(() {
      _todos.add(Todo(text));
      _controller.clear();
    });
  }

  void _toggle(int index, bool? value) {
    setState(() => _todos[index].done = value ?? false);
  }

  void _remove(int index) {
    setState(() => _todos.removeAt(index));
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Việc cần làm')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Nhập công việc...',
                      border: OutlineInputBorder(),
                    ),
                    onSubmitted: (_) => _addTodo(),
                  ),
                ),
                const SizedBox(width: 8),
                FilledButton(onPressed: _addTodo, child: const Text('Thêm')),
              ],
            ),
          ),
          Expanded(
            child: _todos.isEmpty
                ? const Center(child: Text('Chưa có công việc nào'))
                : ListView.builder(
                    itemCount: _todos.length,
                    itemBuilder: (context, index) {
                      final todo = _todos[index];
                      return ListTile(
                        leading: Checkbox(
                          value: todo.done,
                          onChanged: (v) => _toggle(index, v),
                        ),
                        title: Text(
                          todo.title,
                          style: TextStyle(
                            decoration: todo.done
                                ? TextDecoration.lineThrough
                                : null,
                          ),
                        ),
                        trailing: IconButton(
                          icon: const Icon(Icons.delete_outline),
                          onPressed: () => _remove(index),
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}

Giải thích những điểm mấu chốt

  • setState: mỗi lần đổi dữ liệu, bọc thay đổi trong setState để Flutter biết cần vẽ lại.
  • TextEditingController: đọc và xóa nội dung ô nhập; luôn gọi dispose khi không dùng nữa để tránh rò rỉ bộ nhớ.
  • ListView.builder: chỉ dựng các dòng đang hiển thị trên màn hình, rất hiệu quả với danh sách dài.
  • Expanded: cho widget con chiếm phần không gian còn lại trong Row hoặc Column.
  • const: đánh dấu widget bất biến để Flutter tái sử dụng, giúp hiệu năng tốt hơn.

Hot reload: lợi thế lớn của Flutter

Khi ứng dụng đang chạy, sửa code rồi nhấn r trong terminal (hoặc lưu file trong IDE) để thấy thay đổi ngay mà không mất trạng thái hiện tại. Nhấn R cho hot restart khi cần khởi động lại trạng thái. Tính năng này giúp thử nghiệm giao diện cực nhanh.

Bước tiếp theo

  • Lưu dữ liệu bền vững bằng shared_preferences hoặc cơ sở dữ liệu SQLite.
  • Tách code thành nhiều file: model, màn hình, widget con.
  • Học một giải pháp quản lý trạng thái như Provider hoặc Riverpod khi ứng dụng lớn dần.
  • Thêm chế độ tối bằng darkTheme.

Kết luận

Với khoảng 100 dòng Dart, bạn đã có ứng dụng có thể thêm, đánh dấu và xóa công việc. Khi nắm được widget và trạng thái, phần còn lại của Flutter chủ yếu là học thêm các widget và mẫu thiết kế.

Nếu bạn muốn học phát triển ứng dụng di động theo lộ trình rõ ràng, hãy xem các khóa học tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment