+4

Hành trình làm app Flutter từ con số 0 (Phần 3)

Lưu ý: Bài viết chỉ nhằm chia sẻ quá trình tự học Flutter, vì vậy sẽ có rất nhiều thứ ngốk nghếk, sai sót mong anh chị em bỏ qua và chỉ giáo!!!

Tiếp cận widget và một tỷ thứ xung quanh

1.Những widget cơ bản cần ... học thuộc

Trước khi bắt đầu dựng màn hình, mình cũng dành độ tầm vài tiếng để ngồi xem qua document của Flutter. Một số widget bạn sẽ dùng đi dùng lại rất nhiều lần (column, container, row ....), việc đọc trước các thuộc tính của widget sẽ giúp các bạn tiết kiệm được khá nhiều thời gian trong quá trình code. Những cũng đừng lo lắng nếu như ban đầu bạn thấy khó nhằn, cứ phải tra đi tra lại về ý nghĩa của các widget hay các thuộc tính có tác dụng gì, vì việc nó lặp đi lặp lại xuyên suốt quá trình code sẽ khiến bạn ghi nhớ nó.

2.Rõ ràng về bố cục màn hình

Như mình đã chia sẻ ở bài đầu tiên, việc bạn phân chia các thành phần trong màn hình rất quan trọng. Khi đã có bố cục rõ ràng, bạn có thể tạo màn hình nhanh chóng, biết rõ cần dùng widget gì ở đâu... Ngược lại, bạn sẽ gặp rất nhiều khó khăn khi cứ phải dò và thêm thắt các widget! Và mình chính là nạn nhân của điều đó.

Ví dụ đơn cử: image.png

Trong màn hình trên, mới đầu nhìn thì mình sẽ suy nghĩ kiểu: Màn hình chia thành 3 phần, mỗi phần là 1 scroll ngang, trong mỗi scroll ngang là các item, mỗi item thì 1 bên là ảnh, 1 bên là thông tin...... Và cứ thế mình làm thôi, làm xong thì mình phát hiện ra: với cách thiết kế như vậy không hoàn toàn tối ưu, vì sao? Vì màn hình của mình không thể scroll, điều đó dẫn đến những điều bất tiện trong quá trình sử dụng:

  • Nếu kích thước các widget vượt qua kích thước màn hình thì ứng dụng của bạn sẽ bị cắt, bị che khuất 1 phần.
  • Nếu bạn muốn phát triển thêm 1 thành phần nữa, ví dụ mình muốn thêm danh sách những bộ truyện đã được yêu thích chẳng hạn, thì mình sẽ phải ngồi căn chỉnh lại giao diện cho vừa vặn 4 thành phần, bằng không nó sẽ hiện lỗi tràn kích thước màn hình.

Vì vậy mình đã phải mò lại code để thêm SingleChildScrollView bọc toàn bộ các widget lại.

Ngoài ra,việc kết hợp nhiều widget cũng sẽ xảy ra lỗi nếu không sử dụng đúng cách, và cách tốt nhất với những người mới như mình là note lại những lỗi như thế này, vì nó rất dễ gặp và xuất hiện nhiều. Hi vọng việc sử dụng thường xuyên sẽ giúp mình cải thiện, ví dụ mình đã tìm cách kết hợp widget Expanded hoặc Flexible bên trong một Column và nhận được lỗi:

The following assertion was thrown during performLayout():
I/flutter ( 6816): RenderFlex children have non-zero flex but incoming height constraints are unbounded.
I/flutter ( 6816): When a column is in a parent that does not provide a finite height constraint, for example if it is
I/flutter ( 6816): in a vertical scrollable, it will try to shrink-wrap its children along the vertical axis. Setting a
I/flutter ( 6816): flex on a child (e.g. using Expanded) indicates that the child is to expand to fill the remaining
I/flutter ( 6816): space in the vertical direction.
I/flutter ( 6816): These two directives are mutually exclusive. If a parent is to shrink-wrap its child, the child
I/flutter ( 6816): cannot simultaneously expand to fit its parent.
I/flutter ( 6816): Consider setting mainAxisSize to MainAxisSize.min and using FlexFit.loose fits for the flexible
I/flutter ( 6816): children (using Flexible rather than Expanded). This will allow the flexible children to size
I/flutter ( 6816): themselves to less than the infinite remaining space they would otherwise be forced to take, and
I/flutter ( 6816): then will cause the RenderFlex to shrink-wrap the children rather than expanding to fit the maximum
I/flutter ( 6816): constraints provided by the parent.

Thông báo lỗi này thường xuất hiện khi một Column được đặt trong một phần tử cha không cung cấp ràng buộc chiều cao cố định (ví dụ: trong một phần tử cuộn theo chiều dọc), và các thành phần con của Column có yếu tố flex khác 0. Đấy là ChatGPT nói thế, chứ mình chỉ copy lỗi vào chatGPT và làm theo hướng dẫn để fix thôi, một điểm + cho chatGPT là nó sẽ giải thích rõ về nguyên nhân lỗi và đưa ra hướng giải quyết, do vậy bạn sẽ hiểu sâu hơn lỗi gặp phải: image.png

Nothing is more delightful than self-exploration, and there's no greater happiness than earning every success by yourself.

3.Tối ưu, sử dụng lại code

Vấn đề này thì chắc ở mọi ngôn ngữ hay dự án mình đều thấy xuất hiện. Flutter cũng vậy, lúc đầu mới làm mình chưa quen nên chọn cách làm riêng biệt từng phần, sau đó mới tách các widget có thể tái sử dụng. ví dụ như trong ảnh ở trên, mình có sử dụng 2 thanh scroll ngang. Vì vậy mình viết 1 widget ListView để scroll ngang, sau đó ở đâu cần dùng thì mình gọi đến nó truyền thêm widget items và dữ liệu vào:

import 'package:flutter/material.dart';
import '../models/DataModel/book.dart';
class HorizontalList extends StatelessWidget {
  final List<Book> listData; // truyền mảng danh sách sẽ hiển thị.
  final Widget Function(Book)
      itemBuilder; // đây là items sẽ hiển thị.
  const HorizontalList(
      {super.key, required this.listData, required this.itemBuilder});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: listData.length,
      scrollDirection: Axis.horizontal,
      itemBuilder: (context, index) {
        final item = listData[index];
        if (item != null) {
          // Use item safely
          return itemBuilder(item);
        } else {
          // Handle null case if needed
          return const SizedBox(); // or any other widget
        }
      },
    );
  }
}

Và đây là cách mình gọi nó ra

return Expanded(
         child: HorizontalList(
                          listData: books,
                           itemBuilder: (item) {
                                    return CustomNewBookItemWidget(
                                          book: item,
                                          titleNumberLine: 1,
                                          bookBloc:
                                              bookBloc); // Use a custom widget to display each item
                                    },
                             ),
                          )

Những mục dùng chung, hoặc dùng để mô tả cụ thể một widget nào đó mình bỏ vào thư mục widget image.png

4.Sử dụng kiểu dữ liệu rõ ràng cho đối tượng

Dart cũng cấp cách khai báo kiểu dữ liệu cho các đối tượng 1 cách minh bạch. Điều đó hỗ trợ rất nhiều khi mình làm về logic. Mọi thứ sai với quy định đều bị bắt lại ngay lập tức, vì vậy bạn sẽ không sợ bị miss dữ liệu. Ví dụ final List<Book> listData; Mình đã khai báo kiểu dữ liệu cho listData là Book. Vì vậy những phần tử không phải Book sẽ không được xuất hiện trong mảng. Và đây là cách mình khai báo kiểu dữ liệu Book :

class Book {
  int id;
  String name;
  String author;
  double rating;
  double progress;
  String imageUrl;
  int isPicked;
  String keyBook;
  String year;
  int seriesBookId;
  int pagesContinue;
  int millisecondsImport;
  int? isFavorite;

  Book({
    required this.id,
    required this.name,
    required this.author,
    required this.rating,
    required this.progress,
    required this.imageUrl,
    required this.isPicked,
    required this.keyBook,
    required this.year,
    required this.seriesBookId,
    required this.pagesContinue,
    required this.millisecondsImport,
    this.isFavorite,
  });
}

5.Tương thích trên nhiều màn hình

Cái này mình đã có trình bày qua ở phần 2, hiều đơn giản thì mình chia màn hình thành các phần và style dựa trên các phần đó chứ không dựa trên 1 kích thước cụ thể (ví dụ 10, hay 20)

// app_constants.dart

import 'package:flutter/widgets.dart';

class AppConstants {
  final BuildContext context;

  AppConstants(this.context);

  double get minHeight => MediaQuery.of(context).size.height / 812;
  double get minWidth => MediaQuery.of(context).size.width / 375;
}

Mình đã sử dụng phương pháp này và thấy nó ổn, trên cả iOS và android, mình cũng test qua nhiều màn hình độ phân giải khác nhau và nói chung khá hiệu quả.

Tóm tắt

Vấn đề 1: Kích thước màn hình và tỷ lệ khung hình khác nhau.

Giải quyết: Sử dụng các widgets linh hoạt như Expanded, Flexible, MediaQuery, hoặc LayoutBuilder để tạo giao diện có thể thích nghi với nhiều kích thước và tỷ lệ khung hình khác nhau. Ngoài ra bạn có thể sử dụng cách phân chia màn hình mình đã nêu ở trên

Vấn đề 1: Kích thước màn hình và tỷ lệ khung hình khác nhau.

Giải quyết: Sử dụng các widgets linh hoạt như Expanded, Flexible, MediaQuery, hoặc LayoutBuilder để tạo giao diện có thể thích nghi với nhiều kích thước và tỷ lệ khung hình khác nhau. Ngoài ra bạn có thể sử dụng cách phân chia màn hình mình đã nêu ở trên

Vấn đề 2: Hiển thị sai trên các thiết bị với màn hình cỡ lớn.

Giải quyết: Sử dụng SingleChildScrollView hoặc ListView để bao quanh nội dung để tránh lỗi tràn nội dung và xem xét việc sử dụng Wrap hoặc Flow để tự động xếp hàng các phần tử trên màn hình lớn. Chú ý về việc kết hợp các widget vì nó có thể gây ra lỗi. Ghi nhớ và khắc phục nó trong các lần tiếp theo sẽ cải thiện được kỹ năng.

Vấn đề 3: Hiển thị không đúng vị trí trên các thiết bị với notch hoặc nút điều hướng.

Giải quyết: Sử dụng SafeArea để đảm bảo rằng nội dung không bị che khuất bởi notch hoặc nút điều hướng của thiết bị.

Vấn đề 4: Hiển thị không đẹp trên một số thiết bị hoặc màn hình.

Giải quyết: Kiểm tra và xem xét sử dụng các điều kiện và thư viện để điều chỉnh giao diện cho từng trường hợp cụ thể. Sử dụng các phương thức kiểm thử và thiết lập mô phỏng để đảm bảo tương thích.

Vấn đề 5: Tái sử dụng

Giải quyết: Hãy cố gắng tách các thành phần có thể dùng chung, nếu mới bắt đầu như mình thì có thể làm giống như cách mình chia sẻ ở trên. Hơi mất thời gian nhưng sẽ dễ hơn

Vấn đề 6: Khai báo kiểu dữ liệu rõ ràng cho đối tượng

Giải quyết: Bắt đầu sớm với việc này chắc chắn sẽ dễ tiếp cận, và dễ dàng hơn trong quá trình làm việc.

Cảm ơn các bạn đã đọc đến đây, hãy bắt tay dựng luôn một dự án cho bản thân mình. Hi vọng bài viết có thể giúp ích cho bạn.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.