Responsive nâng cao trong Flutter

1. Responsive Columns và Grid System

1.1. Khái niệm Responsive Columns

Responsive Columns là phương pháp chia vùng hiển thị thành các cột để sắp xếp nội dung theo một hệ thống thống nhất. Thay vì đặt từng Widget tại các vị trí cố định, giao diện được tổ chức thành những vùng có tỷ lệ và khoảng cách rõ ràng, sau đó điều chỉnh số lượng cột dựa trên chiều rộng thực tế.

Trong Flutter, Responsive Columns thường được sử dụng để xây dựng Dashboard, trang danh mục sản phẩm, thư viện hình ảnh, danh sách bài viết và các giao diện quản trị. Cách tổ chức này giúp ứng dụng duy trì sự nhất quán khi chạy trên điện thoại, máy tính bảng, laptop và màn hình desktop.

Ví dụ về cách phân chia cột theo kích thước màn hình:

  • Điện thoại: Hiển thị một cột, mỗi Card chiếm gần như toàn bộ chiều rộng khả dụng.

  • Máy tính bảng: Hiển thị hai hoặc ba cột tùy theo độ rộng và nội dung.

  • Desktop: Hiển thị ba, bốn hoặc nhiều cột hơn nếu không gian cho phép.

  • Màn hình lớn: Giới hạn chiều rộng nội dung và căn giữa thay vì kéo giãn tất cả Card.

Ming-Image 0.1 Design: 6B UI and Poster Model in ComfyUI | ComfyUI Wiki

Responsive Admin Dashboard UI by Vanessa on Dribbble

StudyTogether - Responsive Web App Case Study :: Behance

1.2. Các thành phần của Grid System

Một Grid System thường bao gồm ba thành phần chính:

Columns: Các cột dùng để xác định cấu trúc bố cục. Nội dung được đặt trong một hoặc nhiều cột tùy theo kích thước của từng thành phần.

Gutter: Khoảng cách giữa các cột. Gutter giúp các Card không dính sát vào nhau, đồng thời tạo ra khoảng trống để người dùng dễ phân biệt từng vùng nội dung.

Margin: Khoảng cách giữa nội dung và cạnh ngoài của vùng hiển thị. Margin giúp giao diện không bị sát mép màn hình, đặc biệt trên điện thoại hoặc cửa sổ có chiều rộng nhỏ.

Một hệ thống Grid hợp lý cần cân bằng giữa chiều rộng Card, số lượng cột và khoảng cách. Nếu có quá nhiều cột, nội dung bên trong mỗi Card sẽ bị thu hẹp. Ngược lại, nếu có quá ít cột trên màn hình lớn, giao diện có thể lãng phí không gian.

1.3. Xây dựng Grid bằng GridView

Flutter cung cấp GridView để hiển thị các phần tử theo dạng lưới. Trong trường hợp cần thay đổi số lượng cột dựa trên chiều rộng, có thể sử dụng LayoutBuilder kết hợp với SliverGridDelegateWithFixedCrossAxisCount.

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    final products = List.generate(
      12,
      (index) => 'Sản phẩm ${index + 1}',
    );

    return LayoutBuilder(
      builder: (context, constraints) {
        final width = constraints.maxWidth;

        final crossAxisCount = width < 600
            ? 1
            : width < 900
                ? 2
                : width < 1200
                    ? 3
                    : 4;

        return GridView.builder(
          padding: const EdgeInsets.all(16),
          itemCount: products.length,
          gridDelegate:
              SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: crossAxisCount,
            crossAxisSpacing: 16,
            mainAxisSpacing: 16,
            childAspectRatio: 1.2,
          ),
          itemBuilder: (context, index) {
            return Card(
              child: Center(
                child: Text(products[index]),
              ),
            );
          },
        );
      },
    );
  }
}

Trong ví dụ trên, LayoutBuilder cung cấp chiều rộng mà Widget hiện tại có thể sử dụng. Dựa vào giá trị constraints.maxWidth, ứng dụng lựa chọn số cột phù hợp.

Các thuộc tính quan trọng:

  • crossAxisCount: Số cột hiển thị.

  • crossAxisSpacing: Khoảng cách giữa các cột.

  • mainAxisSpacing: Khoảng cách giữa các hàng.

  • childAspectRatio: Tỷ lệ giữa chiều rộng và chiều cao của mỗi phần tử.

  • padding: Khoảng cách giữa nội dung và cạnh ngoài của Grid.

Cách tiếp cận này phù hợp khi số lượng cột được xác định bằng các ngưỡng chiều rộng cụ thể.

1.4. Tự động xác định số cột bằng Max Cross Axis Extent

Trong nhiều giao diện, không nhất thiết phải quy định rõ màn hình 600 px có hai cột hay màn hình 900 px có ba cột. Thay vào đó, có thể xác định chiều rộng tối đa mong muốn của mỗi Card, sau đó để Flutter tự tính số cột.

GridView.builder(
  padding: const EdgeInsets.all(16),
  itemCount: products.length,
  gridDelegate:
      const SliverGridDelegateWithMaxCrossAxisExtent(
    maxCrossAxisExtent: 280,
    crossAxisSpacing: 16,
    mainAxisSpacing: 16,
    childAspectRatio: 0.8,
  ),
  itemBuilder: (context, index) {
    return ProductCard(product: products[index]);
  },
)

maxCrossAxisExtent xác định kích thước tối đa theo trục ngang của mỗi phần tử trong Grid. Flutter dựa trên không gian khả dụng để xác định số cột có thể bố trí.

Phương pháp này phù hợp với danh sách sản phẩm, thư viện hình ảnh và các giao diện có Card tương đối đồng nhất. Tuy nhiên, cần chú ý rằng maxCrossAxisExtent là giới hạn tối đa, không phải lúc nào cũng bảo đảm mỗi Card có đúng chiều rộng được chỉ định.

1.5. Nguyên tắc thiết kế Grid trong Flutter

Khi xây dựng Grid System, cần xem xét đồng thời kích thước phần tử và khả năng đọc nội dung.

  • Không nên tăng số cột chỉ để tận dụng toàn bộ chiều rộng nếu văn bản hoặc hình ảnh trở nên quá nhỏ.

  • Nên sử dụng khoảng cách nhất quán giữa các Card.

  • Cần kiểm tra những nội dung có chiều cao thay đổi, chẳng hạn tên sản phẩm dài hoặc mô tả nhiều dòng.

  • Nên dùng GridView.builder cho danh sách lớn để xây dựng phần tử theo nhu cầu.

  • Cần giới hạn chiều rộng nội dung khi Grid nằm trong màn hình desktop rất rộng.

  • Không nên phụ thuộc hoàn toàn vào tên thiết bị để quyết định số cột; chiều rộng khả dụng mới là cơ sở quan trọng.

Responsive Columns và Grid System giúp giao diện có cấu trúc rõ ràng, dễ mở rộng và dễ bảo trì. Khi kết hợp với LayoutBuilder, GridView và các giới hạn kích thước hợp lý, ứng dụng có thể tự điều chỉnh bố cục mà không cần tạo một giao diện riêng cho từng thiết bị.

2. ConstrainedBox và BoxConstraints

2.1. Khái niệm ConstrainedBox

ConstrainedBox là Widget dùng để áp đặt các giới hạn kích thước lên Widget con. Những giới hạn này có thể quy định chiều rộng tối thiểu, chiều rộng tối đa, chiều cao tối thiểu và chiều cao tối đa.

Trong Flutter, việc xác định kích thước không đơn giản chỉ là đặt một con số cho Widget. Kích thước cuối cùng còn phụ thuộc vào các ràng buộc từ Widget cha, yêu cầu của Widget con và quy tắc bố cục của hệ thống RenderObject.

ConstrainedBox đặc biệt hữu ích trong Responsive Design vì nó giúp tránh hai tình trạng phổ biến:

  • Nội dung mở rộng quá mức trên màn hình desktop.

  • Thành phần giao diện bị thu nhỏ đến mức khó sử dụng trên màn hình hẹp.

Ví dụ, một biểu mẫu đăng nhập có thể chiếm gần hết chiều rộng trên điện thoại nhưng chỉ sử dụng chiều rộng tối đa 420 logical pixels trên desktop.

2.2. Cơ chế hoạt động của BoxConstraints

BoxConstraints mô tả giới hạn chiều rộng và chiều cao mà một Widget được phép sử dụng. Các giới hạn này được thể hiện thông qua bốn thuộc tính:

Thuộc tính Ý nghĩa
minWidth Chiều rộng tối thiểu
maxWidth Chiều rộng tối đa
minHeight Chiều cao tối thiểu
maxHeight Chiều cao tối đa

Chiều rộng và chiều cao của Widget phải nằm trong phạm vi các giới hạn áp dụng, trừ khi bố cục chứa các tình huống đặc biệt như ràng buộc vô hạn hoặc cách xử lý riêng của RenderObject.

Trong quá trình layout, Widget cha truyền constraints xuống Widget con. Widget con xác định kích thước phù hợp trong phạm vi cho phép, sau đó hệ thống bố trí vị trí và kích thước của phần tử trên màn hình.

Cần phân biệt giữa kích thước mong muốn và kích thước được phép. Một Widget có thể yêu cầu chiều rộng 500 pixels, nhưng nếu Widget cha chỉ cho phép tối đa 300 pixels thì kích thước thực tế phải tuân theo các ràng buộc có hiệu lực.

2.3. Sử dụng ConstrainedBox để giới hạn chiều rộng

ConstrainedBox(
  constraints: const BoxConstraints(
    minWidth: 280,
    maxWidth: 420,
  ),
  child: TextField(
    decoration: const InputDecoration(
      labelText: 'Email',
      border: OutlineInputBorder(),
    ),
  ),
)

Ví dụ này đặt giới hạn chiều rộng cho TextField. Tuy nhiên, ConstrainedBox không tự bảo đảm phần tử luôn rộng tối thiểu 280 pixels nếu constraints từ Widget cha không cho phép điều đó. Nếu vùng hiển thị hẹp hơn giới hạn tối thiểu, các ràng buộc có thể xung đột và gây lỗi layout.

Vì vậy, khi áp dụng giới hạn kích thước cho màn hình nhỏ, cần bảo đảm các giá trị tối thiểu tương thích với không gian thực tế.

2.4. Kết hợp ConstrainedBox với Center

Một mẫu bố cục phổ biến là giới hạn chiều rộng nội dung rồi căn giữa trên màn hình lớn.

Scaffold(
  body: Center(
    child: ConstrainedBox(
      constraints: const BoxConstraints(
        maxWidth: 480,
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text(
              'Đăng nhập',
              style: TextStyle(
                fontSize: 28,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 24),
            const TextField(
              decoration: InputDecoration(
                labelText: 'Email',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            const TextField(
              obscureText: true,
              decoration: InputDecoration(
                labelText: 'Mật khẩu',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 24),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () {},
                child: const Text('Đăng nhập'),
              ),
            ),
          ],
        ),
      ),
    ),
  ),
)

Trong trường hợp này, Center giúp căn giữa nội dung trong vùng có không gian dư thừa, còn ConstrainedBox giới hạn chiều rộng tối đa. Padding tạo khoảng cách giữa biểu mẫu và vùng bên ngoài.

Mẫu bố cục này thích hợp cho màn hình đăng nhập, đăng ký, đặt lại mật khẩu, nhập mã xác thực và các biểu mẫu cần duy trì chiều rộng dễ đọc.

2.5. Phân biệt ConstrainedBox, SizedBox và Container

Widget Công dụng chính
ConstrainedBox Áp đặt giới hạn kích thước tối thiểu và tối đa
SizedBox Đặt kích thước cụ thể hoặc tạo khoảng trống
Container Kết hợp nhiều chức năng như padding, màu nền, trang trí, alignment và constraints

Không nên sử dụng SizedBox với kích thước cố định cho mọi thiết bị nếu điều đó có thể làm nội dung tràn khỏi vùng hiển thị. Trong những trường hợp cần linh hoạt theo kích thước màn hình, ConstrainedBox hoặc LayoutBuilder thường thích hợp hơn.

2.6. Những lỗi thường gặp khi sử dụng BoxConstraints

Một số lỗi thường xuất hiện khi xây dựng bố cục phức tạp:

  • Đặt minWidth lớn hơn chiều rộng mà Widget cha có thể cung cấp.

  • Sử dụng Expanded bên trong một vùng có chiều rộng hoặc chiều cao không giới hạn theo trục tương ứng.

  • Đặt kích thước cố định cho nội dung có văn bản thay đổi theo ngôn ngữ.

  • Kết hợp nhiều lớp constraints mà không xác định rõ Widget nào đang kiểm soát kích thước.

  • Giả định rằng maxWidth sẽ luôn làm Widget rộng đến mức tối đa, trong khi nó chỉ xác định giới hạn trên.

Hiểu rõ ConstrainedBox và BoxConstraints giúp lập trình viên kiểm soát bố cục chính xác hơn, hạn chế lỗi tràn giao diện và xây dựng các thành phần có thể tái sử dụng trên nhiều loại màn hình.

3. Center và Max-Width Content

3.1. Căn giữa nội dung trong giao diện Responsive

Center là Widget dùng để căn giữa Widget con trong không gian mà Widget cha cung cấp. Trong Responsive Design, Center thường được kết hợp với ConstrainedBox để tạo ra những vùng nội dung có chiều rộng giới hạn và vị trí cân đối.

Trên điện thoại, nội dung thường chiếm gần hết chiều rộng khả dụng sau khi trừ khoảng cách hai bên. Trên desktop, nếu kéo dài nội dung qua toàn bộ màn hình, văn bản có thể trở nên khó đọc, biểu mẫu mất cân đối và các thành phần giao diện bị phân tán.

Ví dụ, một màn hình desktop có chiều rộng 1920 logical pixels không nhất thiết phải hiển thị biểu mẫu với chiều rộng tương đương 1920 pixels. Thay vào đó, biểu mẫu có thể được giới hạn trong khoảng 400–600 pixels và đặt ở chính giữa màn hình.

Sign in Page by Nacho Sánchez on Dribbble

Hot motivated seller leads marketplace | American Leads

There are eight page layout options - Design interesting one-column websites

3.2. Khái niệm Max-Width Content

Max-Width Content là nguyên tắc giới hạn chiều rộng tối đa của vùng nội dung. Đây là kỹ thuật phổ biến trong thiết kế website, Dashboard, ứng dụng thương mại điện tử và ứng dụng quản lý dữ liệu.

Mục tiêu của Max-Width Content không phải thu nhỏ toàn bộ giao diện mà là kiểm soát chiều rộng của những vùng cần duy trì tính dễ đọc.

Ví dụ:

  • Bài viết có thể giới hạn chiều rộng để tránh mỗi dòng chứa quá nhiều ký tự.

  • Biểu mẫu có thể giới hạn chiều rộng để các trường nhập liệu không quá dài.

  • Trang sản phẩm có thể sử dụng vùng nội dung rộng hơn biểu mẫu đăng nhập.

  • Dashboard có thể sử dụng nhiều cột nhưng vẫn giới hạn tổng chiều rộng của vùng hiển thị.

  • Trang cài đặt có thể đặt nội dung ở giữa màn hình, trong khi thanh điều hướng vẫn chiếm toàn bộ chiều cao hoặc chiều rộng cần thiết.

Giá trị maxWidth nên được lựa chọn theo đặc điểm nội dung, không nên sử dụng một con số cố định cho tất cả các thành phần trong ứng dụng.

3.3. Xây dựng Max-Width Content bằng Flutter

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Trang nội dung'),
      ),
      body: Center(
        child: ConstrainedBox(
          constraints: const BoxConstraints(
            maxWidth: 1100,
          ),
          child: Padding(
            padding: const EdgeInsets.symmetric(
              horizontal: 24,
              vertical: 32,
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Tổng quan hệ thống',
                  style: Theme.of(context)
                      .textTheme
                      .headlineMedium,
                ),
                const SizedBox(height: 16),
                const Text(
                  'Khu vực này chứa thông tin tổng quan, '
                  'các chỉ số và dữ liệu quan trọng của ứng dụng.',
                ),
                const SizedBox(height: 24),
                const Card(
                  child: Padding(
                    padding: EdgeInsets.all(24),
                    child: Text(
                      'Nội dung Dashboard',
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Trong ví dụ này, ConstrainedBox giới hạn chiều rộng tối đa của vùng nội dung ở mức 1100 pixels. Center căn giữa vùng đó khi màn hình rộng hơn giới hạn. Padding tạo khoảng cách bên trong để nội dung không áp sát cạnh.

Trên màn hình nhỏ, nội dung vẫn có thể co lại theo constraints của Widget cha. Tuy nhiên, khi xây dựng ứng dụng thực tế, cần bảo đảm phần padding không chiếm quá nhiều không gian khả dụng trên điện thoại.

3.4. Phân biệt chiều rộng màn hình và chiều rộng nội dung

Hai giá trị này có vai trò khác nhau:

  • Screen Width: Chiều rộng của màn hình hoặc cửa sổ ứng dụng.

  • Available Width: Chiều rộng thực tế mà một Widget được phép sử dụng sau khi xét đến bố cục cha, padding và các vùng lân cận.

  • Content Max Width: Chiều rộng tối đa mà vùng nội dung được phép đạt tới.

Trong một Dashboard có thanh điều hướng bên trái rộng 250 pixels, chiều rộng khả dụng cho nội dung chính không còn bằng toàn bộ chiều rộng cửa sổ. Vì vậy, không nên luôn lấy kích thước màn hình làm cơ sở duy nhất để xác định kích thước Widget.

LayoutBuilder có thể được sử dụng để đọc constraints tại vị trí cụ thể trong Widget Tree, từ đó tính toán chiều rộng phù hợp với không gian thực tế.

3.5. Nguyên tắc thiết kế Max-Width Content

Khi triển khai kỹ thuật này, cần chú ý:

  • Giới hạn chiều rộng dựa trên mục đích của từng vùng nội dung.

  • Sử dụng Center khi vùng nội dung cần nằm giữa màn hình.

  • Giữ khoảng cách hai bên phù hợp với màn hình nhỏ.

  • Không giới hạn quá chặt những giao diện cần hiển thị bảng dữ liệu rộng.

  • Có thể sử dụng các giới hạn khác nhau cho bài viết, biểu mẫu, danh sách và Dashboard.

  • Kết hợp với SingleChildScrollView khi nội dung có thể vượt quá chiều cao khả dụng.

  • Tránh đặt chiều rộng cố định cho toàn bộ trang nếu ứng dụng cần hỗ trợ nhiều kích thước cửa sổ.

Center và Max-Width Content giúp giao diện desktop cân đối hơn, đồng thời duy trì khả năng sử dụng trên điện thoại và máy tính bảng.

4. Orientation-Aware Layout

4.1. Khái niệm Orientation-Aware Layout

Orientation-Aware Layout là phương pháp điều chỉnh bố cục dựa trên hướng hiển thị của vùng giao diện. Hai hướng phổ biến là:

  • Portrait: Hướng dọc, chiều cao lớn hơn chiều rộng.

  • Landscape: Hướng ngang, chiều rộng lớn hơn chiều cao.

Trong Flutter, có thể sử dụng OrientationBuilder để nhận biết hướng của vùng bố cục hoặc sử dụng LayoutBuilder để đọc chiều rộng và chiều cao khả dụng.

Orientation-Aware Layout thường được sử dụng trong ứng dụng xem video, đọc tài liệu, chỉnh sửa hình ảnh, chơi game, Dashboard và ứng dụng có nội dung cần thay đổi cách sắp xếp theo hướng màn hình.

Responsive Admin Dashboard UI by Vanessa on Dribbble

Sobre relatórios do Power BI otimizados para dispositivos móveis - Power BI | Microsoft Learn

Build an app with an adaptive layout  |  Android Developers

4.2. Cơ chế hoạt động của OrientationBuilder

OrientationBuilder cung cấp thông tin về hướng của vùng bố cục thông qua tham số orientation trong callback builder.

OrientationBuilder(
  builder: (context, orientation) {
    if (orientation == Orientation.portrait) {
      return const Column(
        children: [
          Expanded(
            child: ColoredBox(
              color: Colors.blue,
              child: Center(
                child: Text('Nội dung'),
              ),
            ),
          ),
          Expanded(
            child: ColoredBox(
              color: Colors.orange,
              child: Center(
                child: Text('Chi tiết'),
              ),
            ),
          ),
        ],
      );
    }

    return const Row(
      children: [
        Expanded(
          child: ColoredBox(
            color: Colors.blue,
            child: Center(
              child: Text('Nội dung'),
            ),
          ),
        ),
        Expanded(
          child: ColoredBox(
            color: Colors.orange,
            child: Center(
              child: Text('Chi tiết'),
            ),
          ),
        ),
      ],
    );
  },
)

Trong ví dụ trên, giao diện sử dụng Column ở hướng dọc và Row ở hướng ngang. Hai vùng nội dung được sắp xếp theo chiều khác nhau để tận dụng không gian hiển thị.

Expanded chia phần không gian khả dụng cho các thành phần con. Tuy nhiên, các Widget này phải nằm trong bố cục có giới hạn phù hợp theo trục mở rộng; nếu không, Flutter có thể báo lỗi về constraints.

4.3. OrientationBuilder và LayoutBuilder

Hai Widget có mục đích liên quan nhưng không hoàn toàn giống nhau.

Tiêu chí OrientationBuilder LayoutBuilder
Thông tin chính Hướng dọc hoặc ngang Constraints tại vị trí bố cục
Dữ liệu cung cấp Orientation BoxConstraints
Phù hợp với Thay đổi bố cục theo hướng Thay đổi bố cục theo kích thước
Độ linh hoạt Dựa trên hướng Dựa trên chiều rộng và chiều cao khả dụng

Một màn hình ngang không nhất thiết phải có bố cục dành riêng cho desktop. Ví dụ, điện thoại xoay ngang có thể chỉ rộng 850 pixels, trong khi cửa sổ desktop có thể rộng 1400 pixels.

Vì vậy, trong các giao diện cần thích nghi chính xác, nên xem xét cả hướng và kích thước thực tế. Nếu chỉ cần thay đổi số cột, chiều rộng Card hoặc cấu trúc thanh điều hướng, LayoutBuilder thường là lựa chọn phù hợp hơn.

4.4. Ví dụ thiết kế giao diện thích nghi theo hướng và chiều rộng

LayoutBuilder(
  builder: (context, constraints) {
    final isWide = constraints.maxWidth >= 700;

    final image = Container(
      height: isWide ? 280 : 200,
      color: Colors.blueGrey,
      child: const Center(
        child: Icon(
          Icons.image,
          size: 64,
          color: Colors.white,
        ),
      ),
    );

    final details = const Padding(
      padding: EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'Thông tin sản phẩm',
            style: TextStyle(
              fontSize: 22,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 12),
          Text(
            'Tên sản phẩm, mô tả, giá bán '
            'và các thông tin liên quan.',
          ),
        ],
      ),
    );

    if (isWide) {
      return Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(child: image),
          Expanded(child: details),
        ],
      );
    }

    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        image,
        details,
      ],
    );
  },
)

Đây là một ví dụ về việc thay đổi cấu trúc bố cục dựa trên chiều rộng thay vì chỉ dựa vào hướng màn hình. Khi vùng hiển thị đủ rộng, hình ảnh và thông tin sản phẩm nằm cạnh nhau. Khi không gian bị giới hạn, các thành phần được xếp theo chiều dọc.

4.5. Những nguyên tắc cần tuân thủ

  • Không giả định thiết bị có một hướng hiển thị cố định.

  • Không xem hướng ngang đồng nghĩa với tablet hoặc desktop.

  • Kiểm tra khả năng hiển thị khi bàn phím ảo xuất hiện.

  • Tránh bố cục có chiều cao cố định nếu nội dung có thể tăng theo kích thước chữ.

  • Đảm bảo các nút, trường nhập liệu và thao tác chính vẫn dễ sử dụng ở cả hai hướng.

  • Kiểm tra trên thiết bị có tỷ lệ màn hình khác nhau.

Orientation-Aware Layout giúp ứng dụng tận dụng không gian tốt hơn, nhưng nên được kết hợp với Responsive Design dựa trên constraints để tránh phụ thuộc quá nhiều vào hướng hiển thị.

5. Dynamic Window Resizing

5.1. Khái niệm Dynamic Window Resizing

Dynamic Window Resizing là khả năng thích nghi với việc thay đổi kích thước cửa sổ trong lúc ứng dụng đang chạy.

Kỹ thuật này đặc biệt quan trọng đối với Flutter trên desktop và web, nơi người dùng có thể kéo cạnh cửa sổ, thay đổi kích thước trình duyệt, bật chế độ toàn màn hình hoặc đặt nhiều ứng dụng cạnh nhau.

Trong môi trường desktop, kích thước vùng làm việc có thể thay đổi liên tục. Một ứng dụng ban đầu có chiều rộng 1400 pixels có thể bị thu nhỏ còn 900 pixels khi người dùng mở thêm một cửa sổ. Nếu bố cục chỉ được thiết kế cho kích thước ban đầu, nội dung có thể bị tràn hoặc trở nên khó sử dụng.

5.2. Cơ chế cập nhật bố cục

Flutter sử dụng hệ thống layout để tính toán lại kích thước và vị trí của các thành phần khi constraints thay đổi.

Các Widget như LayoutBuilder, Expanded, Flexible, Wrap và GridView có thể hỗ trợ xây dựng bố cục thích nghi.

Ví dụ, một Dashboard có thể thay đổi từ bốn cột thành hai cột khi cửa sổ bị thu hẹp. Thanh điều hướng có thể chuyển từ dạng cố định sang dạng thu gọn, còn các Card có thể giảm số lượng hiển thị trên mỗi hàng.

Cần phân biệt giữa thay đổi kích thước cửa sổ và thay đổi dữ liệu. Việc cửa sổ rộng hơn không đồng nghĩa dữ liệu cần tải lại từ máy chủ. Thông thường, chỉ bố cục cần được tính toán lại; dữ liệu nên được quản lý độc lập để tránh các yêu cầu mạng không cần thiết.

5.3. Ví dụ Dashboard tự điều chỉnh theo chiều rộng

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    final items = [
      'Doanh thu',
      'Đơn hàng',
      'Khách hàng',
      'Sản phẩm',
    ];

    return Scaffold(
      appBar: AppBar(
        title: const Text('Dashboard'),
      ),
      body: LayoutBuilder(
        builder: (context, constraints) {
          final width = constraints.maxWidth;

          final columns = width < 600
              ? 1
              : width < 1000
                  ? 2
                  : 4;

          return GridView.builder(
            padding: const EdgeInsets.all(16),
            itemCount: items.length,
            gridDelegate:
                SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: columns,
              crossAxisSpacing: 16,
              mainAxisSpacing: 16,
              childAspectRatio: 1.4,
            ),
            itemBuilder: (context, index) {
              return Card(
                child: Center(
                  child: Text(
                    items[index],
                    style: const TextStyle(
                      fontSize: 18,
                    ),
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

Khi cửa sổ thay đổi kích thước, LayoutBuilder có thể nhận constraints mới và lựa chọn số cột tương ứng. Flutter sẽ bố trí lại Grid theo kích thước mới.

Các ngưỡng trong ví dụ chỉ mang tính minh họa. Khi triển khai ứng dụng thực tế, cần thử nghiệm với kích thước cửa sổ nhỏ nhất mà ứng dụng hỗ trợ, độ dài nội dung và yêu cầu sử dụng của từng loại màn hình.

5.4. Quản lý trạng thái khi cửa sổ thay đổi kích thước

Responsive Layout không chỉ liên quan đến việc sắp xếp Widget. Một ứng dụng hoàn chỉnh còn cần bảo toàn trạng thái người dùng trong quá trình bố cục thay đổi.

Ví dụ:

  • Nội dung đang nhập trong biểu mẫu không bị mất khi đổi kích thước.

  • Vị trí cuộn của danh sách không bị đặt lại ngoài ý muốn.

  • Tab đang mở được giữ nguyên.

  • Dữ liệu Dashboard không bị tải lại không cần thiết.

  • Các thao tác đang thực hiện không bị gián đoạn chỉ vì bố cục chuyển từ một cột sang hai cột.

Một nguyên tắc quan trọng là tách logic dữ liệu và trạng thái khỏi logic hiển thị. Có thể sử dụng StatefulWidget, Provider, Riverpod hoặc BLoC tùy theo kiến trúc ứng dụng.

Không nên khởi tạo lại toàn bộ trạng thái bên trong callback builder chỉ vì constraints thay đổi. Những đối tượng có vòng đời riêng như TextEditingController, FocusNode và ScrollController cần được quản lý đúng cách.

5.5. Kiểm thử Dynamic Window Resizing

Khi kiểm thử, cần thay đổi kích thước cửa sổ qua nhiều mức, thay vì chỉ chạy ứng dụng ở kích thước mặc định.

Các tình huống nên kiểm tra:

  • Cửa sổ rộng và cao.

  • Cửa sổ hẹp nhưng vẫn đủ không gian cho các thao tác chính.

  • Cửa sổ có chiều cao thấp.

  • Cửa sổ bị thu nhỏ khi thanh điều hướng vẫn đang mở.

  • Biểu mẫu có nội dung dài.

  • Bảng dữ liệu có nhiều cột.

  • Văn bản tăng kích thước theo cài đặt trợ năng.

Ngoài việc kiểm tra giao diện có bị tràn hay không, cần xác nhận các thao tác vẫn hoạt động chính xác. Một giao diện có thể không xuất hiện lỗi overflow nhưng vẫn khó sử dụng nếu nút quan trọng bị đẩy ra ngoài vùng nhìn thấy.

Dynamic Window Resizing giúp ứng dụng Flutter trở nên linh hoạt hơn trong môi trường đa cửa sổ và nhiều kích thước hiển thị.

6. Split View và Multi-Window Layout

6.1. Khái niệm Split View

Split View là cách tổ chức giao diện thành nhiều vùng hiển thị đồng thời trong cùng một cửa sổ hoặc vùng làm việc. Thay vì chuyển đổi qua lại giữa các màn hình, người dùng có thể quan sát danh sách, nội dung chi tiết và các thao tác liên quan cùng lúc.

Ví dụ phổ biến là giao diện email với danh sách thư ở bên trái và nội dung thư ở bên phải. Những ứng dụng quản lý sản phẩm có thể hiển thị danh mục sản phẩm ở một vùng và thông tin chi tiết ở vùng còn lại.

Trong Flutter, Split View thường được xây dựng bằng Row, Column, Expanded, Flexible và LayoutBuilder. Với các ứng dụng desktop phức tạp, có thể bổ sung khả năng điều chỉnh kích thước từng vùng, quản lý trạng thái lựa chọn và lưu cấu hình bố cục.

Adding a Toolbar | Apple Developer Documentation

Introducing the Airtable iPad app

Exercice : créer une application Sports  |  Android Developers

6.2. Khái niệm Multi-Window Layout

Multi-Window Layout là cách thiết kế ứng dụng có thể hoạt động trong môi trường nhiều cửa sổ hoặc nhiều vùng ứng dụng cùng tồn tại.

Cần phân biệt hai khái niệm:

  • Split View: Chia không gian hiển thị của một giao diện thành nhiều vùng nội dung.

  • Multi-Window: Cho phép ứng dụng hoặc nền tảng làm việc với nhiều cửa sổ riêng biệt, tùy theo hệ điều hành và khả năng triển khai.

Việc sắp xếp hai bảng nội dung bằng Row không tự động tạo ra hai cửa sổ độc lập. Nó chỉ tạo ra hai vùng trong cùng một cây Widget.

Khả năng quản lý cửa sổ độc lập phụ thuộc vào nền tảng, cấu hình ứng dụng và các API hoặc giải pháp hỗ trợ tương ứng. Vì vậy, khi xây dựng ứng dụng Flutter đa nền tảng, cần xác định rõ yêu cầu: chỉ cần chia vùng giao diện hay thực sự cần mở nhiều cửa sổ riêng biệt.

6.3. Xác định không gian ứng dụng thực tế

Khi triển khai Split View, không nên chỉ dựa vào kích thước tổng của thiết bị. Chiều rộng khả dụng có thể bị giảm bởi:

  • Thanh điều hướng cố định.

  • Sidebar.

  • Thanh công cụ.

  • Vùng hiển thị của một ứng dụng khác.

  • Cửa sổ trình duyệt hoặc cửa sổ desktop đã bị thu hẹp.

  • Padding và các khoảng cách nội bộ.

Một ứng dụng chạy trên màn hình rộng có thể vẫn cần bố cục một cột nếu cửa sổ của ứng dụng chỉ chiếm một phần nhỏ màn hình.

Do đó, LayoutBuilder là công cụ hữu ích để quyết định khi nào nên sử dụng Split View. Khi không gian không đủ, giao diện có thể quay về bố cục một cột hoặc chỉ hiển thị một vùng tại một thời điểm.

6.4. Xây dựng giao diện Master-Detail

Master-Detail là một dạng Split View trong đó vùng Master chứa danh sách các đối tượng, còn vùng Detail hiển thị thông tin chi tiết của đối tượng được chọn.

import 'package:flutter/material.dart';

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

  @override
  State<MasterDetailPage> createState() =>
      _MasterDetailPageState();
}

class _MasterDetailPageState
    extends State<MasterDetailPage> {
  int selectedIndex = 0;

  final items = List.generate(
    10,
    (index) => 'Sản phẩm ${index + 1}',
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Quản lý sản phẩm'),
      ),
      body: LayoutBuilder(
        builder: (context, constraints) {
          final isWide = constraints.maxWidth >= 800;

          final listPanel = ListView.builder(
            itemCount: items.length,
            itemBuilder: (context, index) {
              return ListTile(
                selected: selectedIndex == index,
                title: Text(items[index]),
                onTap: () {
                  setState(() {
                    selectedIndex = index;
                  });
                },
              );
            },
          );

          final detailPanel = Center(
            child: Padding(
              padding: const EdgeInsets.all(24),
              child: Text(
                'Chi tiết: ${items[selectedIndex]}',
                style: Theme.of(context)
                    .textTheme
                    .headlineSmall,
              ),
            ),
          );

          if (isWide) {
            return Row(
              children: [
                SizedBox(
                  width: 300,
                  child: listPanel,
                ),
                const VerticalDivider(width: 1),
                Expanded(
                  child: detailPanel,
                ),
              ],
            );
          }

          return listPanel;
        },
      ),
    );
  }
}

Trong ví dụ này, giao diện sử dụng ngưỡng 800 pixels để chuyển đổi giữa hai cách bố trí.

Trên màn hình rộng, danh sách sản phẩm nằm bên trái và thông tin chi tiết nằm bên phải. Trên màn hình hẹp, ứng dụng chỉ hiển thị danh sách để tránh làm mỗi vùng trở nên quá nhỏ.

Ví dụ tập trung minh họa cấu trúc bố cục. Trong ứng dụng thực tế, phiên bản dành cho màn hình nhỏ thường cần điều hướng sang màn hình chi tiết khi người dùng chọn một sản phẩm, đồng thời cung cấp thao tác quay lại danh sách.

6.5. Quản lý trạng thái lựa chọn giữa các vùng

Split View cần duy trì mối liên hệ giữa các vùng nội dung. Khi người dùng chọn một đối tượng trong danh sách, vùng chi tiết phải cập nhật tương ứng.

Những trạng thái thường cần quản lý gồm:

  • Đối tượng hiện đang được chọn.

  • Vị trí cuộn của danh sách.

  • Bộ lọc và từ khóa tìm kiếm.

  • Trạng thái mở rộng hoặc thu gọn của sidebar.

  • Dữ liệu đã tải của vùng chi tiết.

  • Trạng thái chỉnh sửa chưa lưu.

Đối với ứng dụng lớn, nên tách dữ liệu và trạng thái nghiệp vụ khỏi Widget hiển thị. Điều này giúp tránh việc mất dữ liệu khi thay đổi cấu trúc giao diện hoặc chuyển giữa chế độ Split View và bố cục một cột.

6.6. Khả năng thích nghi với không gian hạn chế

Một lỗi phổ biến là cố gắng hiển thị đồng thời tất cả vùng nội dung trên màn hình nhỏ. Nếu danh sách, bộ lọc và chi tiết đều cần chiều rộng lớn, việc đặt chúng cạnh nhau có thể làm giảm khả năng đọc và thao tác.

Giải pháp phù hợp là xác định các chức năng quan trọng nhất tại từng kích thước:

  • Trên màn hình nhỏ, ưu tiên nội dung chính và điều hướng rõ ràng.

  • Trên màn hình trung bình, có thể hiển thị danh sách và chi tiết nếu đủ không gian.

  • Trên màn hình lớn, có thể bổ sung sidebar, vùng chi tiết và bảng công cụ.

  • Trên cửa sổ rất hẹp, cần bảo đảm các nút thao tác và nội dung không bị cắt.

6.7. Kiểm thử Split View và Multi-Window

Quá trình kiểm thử cần xác nhận cả tính đúng đắn của bố cục lẫn khả năng tương tác.

Các tình huống nên kiểm tra gồm:

  • Chuyển đổi giữa màn hình hẹp và rộng.

  • Chọn một đối tượng khác trong danh sách.

  • Thu nhỏ cửa sổ khi vùng chi tiết đang mở.

  • Mở bàn phím ảo khi chỉnh sửa dữ liệu.

  • Cuộn danh sách dài.

  • Giữ trạng thái chỉnh sửa khi bố cục thay đổi.

  • Kiểm tra điều hướng trên điện thoại và máy tính bảng.

  • Xác minh hành vi cửa sổ riêng biệt nếu ứng dụng có yêu cầu Multi-Window thực sự.

Split View và Multi-Window Layout giúp ứng dụng Flutter hỗ trợ những quy trình làm việc phức tạp hơn. Khi kết hợp với Responsive Layout và quản lý trạng thái phù hợp, ứng dụng có thể cung cấp trải nghiệm nhất quán trên nhiều loại thiết bị và môi trường làm việc.

7. Tổng kết Responsive nâng cao trong Flutter

Responsive nâng cao tập trung vào khả năng tổ chức và điều chỉnh giao diện theo không gian hiển thị thực tế. Các kỹ thuật trong nhóm này bổ sung cho những kiến thức nền tảng về Row, Column, Expanded, Flexible, Padding, Container và LayoutBuilder.

  • Responsive Columns và Grid System: Tổ chức nội dung thành hệ thống cột, kiểm soát gutter và số lượng Card.

  • ConstrainedBox và BoxConstraints: Giới hạn kích thước để tránh nội dung quá rộng hoặc không phù hợp với vùng hiển thị.

  • Center và Max-Width Content: Căn giữa nội dung và duy trì chiều rộng dễ đọc trên màn hình lớn.

  • Orientation-Aware Layout: Điều chỉnh cách sắp xếp theo hướng hiển thị, kết hợp với chiều rộng thực tế.

  • Dynamic Window Resizing: Xử lý việc cửa sổ thay đổi kích thước trong khi ứng dụng đang hoạt động.

  • Split View và Multi-Window Layout: Tổ chức nhiều vùng nội dung và hỗ trợ quy trình làm việc đa vùng.

Nguyên tắc quan trọng nhất là thiết kế dựa trên không gian khả dụng và nhu cầu của nội dung, thay vì chỉ phân loại giao diện theo tên thiết bị. Khi áp dụng đúng các nguyên tắc này, lập trình viên có thể xây dựng giao diện Flutter linh hoạt, dễ bảo trì và hạn chế lỗi bố cục trên điện thoại, máy tính bảng, desktop và web.

Bài viết khác

WordPress eCommerce with WooCommerce

WooCommerce là gì? WooCommerce là một plugin mã nguồn mở của WordPress có toàn bộ chức năng tuyệt vời mà website bán hàng cần có như: số lượng hàng hóa, giỏ đơn hàng, đăng ký khách hàng, thanh toán, khuyến mãi… hỗ trợ thiết kế trang e-Commerce nhanh chóng. Các tính năng nổi bật của […]

WordPress plugin

Plugin là gì? Plugin chỉ đơn giản là một chương trình, ứng dụng bổ sung được viết ra để tích hợp vào trong website WordPress, giúp mở rộng chức năng hiện có hoặc thêm một tính năng mới vào website WordPress. Cho phép người dùng WordPress thêm các tính năng bổ sung vào website của họ mà […]

Theme wordpress

Theme wordpress là gì? Trong WordPress, một theme được hiểu là một bộ giao diện mà bạn có thể áp dụng cho trang web của mình. Theme định rõ phong cách mà trang web của bạn đang theo đuổi, đồng bộ cách hiển thị nội dung, hình ảnh, video và giúp tổng thể trở nên […]

WordPress

WordPress là gì? WordPress là mã nguồn mở được viết bằng ngôn ngữ lập trình PHP và sử dụng hệ quản trị cơ sở dữ liệu MySQL. Đây là một công cụ tạo trang web miễn phí, bạn chỉ cần cài lên host và chọn Theme là đã có ngay một trang web tiêu chuẩn rồi. Nếu muốn […]

Tổng hợp xử lý các lỗi cài đặt phần mềm hỗ trợ lập trình website

Phần mềm Xampp (cài server ảo ) https://www.apachefriends.org/download.html Phần mềm appserv Download Lỗi cài đặt & khởi động xampp trên ios Thử tải các phiên bản cũ hơn và cũ hơn hơn cho đến khi có thể cài đặt được Lỗi Access Forbidden Error 403 khi bấm vào phpMyAdmin của xampp ( iOS ) Bật […]

Lập trình website – những kiến thức chung đầu tiên

Từ khoá quan trọng liên quan đến lập trình website Một số từ khoá bạn có thể tham khảo để học lập trình website Lap trinh website co ban html co ban css co ban javascript co ban php co ban lap website trong 1 gio wordpress co ban xay dung website voi wordpreess trong 1 gio […]

Leave a Reply

Your email address will not be published. Required fields are marked *