Flutter cơ bản: Cách phân chia Widget

Trong bài viết này, chúng ta sẽ tìm hiểu kỹ hơn một số Widget được cung cấp bởi package Flutter Material. Chúng ta cũng sẽ học cách có thể tạo các Widget tùy chỉnh của riêng mình và cách chúng hoạt động. Cùng với đó, chúng ta sẽ ứng dụng chúng vào một app thú

Trong bài viết này, chúng ta sẽ tìm hiểu kỹ hơn một số Widget được cung cấp bởi package Flutter Material. Chúng ta cũng sẽ học cách có thể tạo các Widget tùy chỉnh của riêng mình và cách chúng hoạt động. Cùng với đó, chúng ta sẽ ứng dụng chúng vào một app thú vị. Bắt đầu thôi nào!!

Bạn có thể xem thêm bài viết tại: https://200lab.io/blog/flutter-co-ban-cach-phan-chia-widget/

Giới thiệu app demo

Đây chỉ giản là app có một màn hình và chỉ bao gồm các Stateless Widget. Còn gì tuyệt hơn việc tạo app cho chính bạn để bạn để có thể giới thiệu các app của bạn trong tương lai đúng không nào.

Ngoài ra, hãy lưu ý rằng khi nhấn vào nút Website, Call Me hoặc Email Me, nó sẽ lập tức mở ra Browser mặc định, Phone app hoặc Email app. Ngon lành! Chúng ta hãy xem bằng cách nào mà chúng ta có thể xây dựng được app thế này.

Hướng dẫn làm app Portfolio

Khởi tạo dự án

Tạo một ứng dụng Flutter mới bằng cách sử dụng lệnh flutter create <your_app_name> hoặc để biết thêm chi tiết, hãy tham khảo bài viết dưới đây:

https://200lab.io/blog/flutter-co-ban-xay-dung-ung-dung-dau-tien/

Khi app đã được tạo, hãy xóa tất cả nội dung trong main.dart để bắt đầu lại từ đầu.

Chúng ta cần một package (hoặc thư viện) có thể gọi vào các app khác của native. Package này được gọi là url_launcher. Để cài đặt package này, hãy mở file pubspec.yaml và trong phần dependencies hãy thêm url_launcher: ^ 5.7.10 như sau:

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.0
  url_launcher: ^5.7.10

Đây là cấu trúc của file YAML do đó nó cần khai báo đúng cách khoảng trắng/thụt đầu dòng. Vì vậy, hãy đảm bảo rằng bạn làm chính xác như trên. Chi tiết về package ban có thể xem tại đây.

Tạo bố cục cơ bản

Với package được cài đặt, chúng ta có thể tiếp tục với việc tạo bố cục cơ bản của app. Trong file main.dart, hãy import file material.dart từ material package. Sau đó khai báo hàm main và tạo một Stateless Widget mới có tên MyApp. File main.dart của bạn sẽ trông như thế này:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Portfolio App',
      theme: ThemeData(
        primarySwatch: Colors.blueGrey,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Text('We are building this awesome app!Yay!!!!'),
    );
  }
}

Đây là phần bố cục, layout cơ bản của ứng dụng của chúng ta và khi chạy ứng dụng, bạn sẽ thấy màn hình màu đen với dòng chữ màu đỏ. Bây giờ, chúng ta cần bổ sung các style vào cho chúng.

Scaffold

Scaffold Widget được cung cấp bởi material package cung cấp một số thông tin nhất định. Trong đó có appBar, body, drawer, v.v. Ví dụ để xử dụng thanh nav bar, chúng ta dùng AppBar widget truyền vào cho nó:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Portfolio App',
      theme: ThemeData(
        primarySwatch: Colors.blueGrey,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Portfolio'),
          centerTitle: true,
        ),
        body: SingleChildScrollView(
          child: Column(),
        ),
      ),
    );
  }
}

Tới đây, bạn đã có thể thấy một app bar và phần nội dung trắng trơn (empty body). Các SingleChildScrollViewColumn widget cũng được cung cấp bởi material package. SingleChildScrollView giúp cho child của nó có thể scroll được và Column là một widget để layout các child của nó được căn chỉnh từ trên xuống dưới theo mặc định.

Finish app

Code được xây dựng đầy đủ của chúng ta trông như thế này:

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  _launchNativeApps(String url) async {
    if (await canLaunch(url)) {
      await launch(url);
    } else {
      throw 'Could not launch $url';
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Portfolio App',
      theme: ThemeData(
        primarySwatch: Colors.blueGrey,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Scaffold(
        backgroundColor: Color.fromRGBO(227, 234, 237, 1),
        appBar: AppBar(
          title: Text(
            'Portfolio',
          ),
          centerTitle: true,
        ),
        body: SingleChildScrollView(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children:[
              SizedBox(height: 30),
              CircleAvatar(
                radius: 100,
                backgroundColor: Theme.of(context).primaryColor,
                child: CircleAvatar(
                  radius: 95,
                  backgroundColor: Colors.white70,
                  backgroundImage:
                      NetworkImage('https://i.ibb.co/n3RzK2L/shashank.jpg'),
                ),
              ),
              SizedBox(height: 20),
              Text(
                'Shashank Biplav',
                style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700),
              ),
              Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
                child: Divider(),
              ),
              Text(
                'SOFTWARE ENGINEER & TECH BLOGGER',
                style: TextStyle(fontSize: 19, fontWeight: FontWeight.w700),
              ),
              Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 20, vertical: 0),
                child: Divider(),
              ),
              Padding(
                padding: const EdgeInsets.all(30.0),
                child: Text(
                  'Hello, I am Shashank Biplav a Full-Stack developer based in India specializing in building apps for Mobile and the Web.',
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 18),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30),
                child: RaisedButton(
                  onPressed: () {
                    _launchNativeApps(
                        'https://shashankbiplav.com');
                  },
                  child: Container(
                    height: 40,
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Icon(
                          Icons.person_outline_rounded,
                          size: 30,
                          color: Colors.blue[800],
                        ),
                        Text(
                          'Website',
                          style: TextStyle(
                              fontWeight: FontWeight.w600,
                              fontSize: 15,
                              color: Colors.grey[700]),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
              SizedBox(height: 20),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30),
                child: RaisedButton(
                  onPressed: () {
                    _launchNativeApps('tel:+917004026852');
                  },
                  child: Container(
                    height: 40,
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Icon(
                          Icons.wifi_calling_rounded,
                          size: 30,
                          color: Colors.greenAccent[700],
                        ),
                        SizedBox(width: 20),
                        Text(
                          '+91- 700-402-6852',
                          style: TextStyle(
                              fontWeight: FontWeight.w600,
                              fontSize: 20,
                              color: Colors.grey[700]),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
              SizedBox(height: 20),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30),
                child: RaisedButton(
                  onPressed: () {
                    _launchNativeApps(
                        'mailto:[email protected]?subject=I would like to build an app with you!&body=Hi there,n I have an awesome app idea,nn');
                  },
                  child: Container(
                    height: 40,
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Icon(
                          Icons.email_rounded,
                          size: 30,
                          color: Colors.red[800],
                        ),
                        Text(
                          '[email protected]',
                          style: TextStyle(
                              fontWeight: FontWeight.w600,
                              fontSize: 15,
                              color: Colors.grey[700]),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
              SizedBox(height: 20),
            ],
          ),
        ),
      ),
    );
  }
}

WOW! File này khá là lớn. Chúng ta sẽ phải chia nó thành các file nhỏ hơn, tức là các Widget sau này nhưng tạm thời bây giờ chúng ta hãy hiểu điều gì đang xảy ra ở đây. Tất cả các Widget được sử dụng ở trên vẫn đều được cung cấp bởi material package.

SizedBox Widget

Widget này bình thường sẽ không nhìn thấy trên giao diện trừ khi có một widget hiển thị nào đó được truyền vào. Ở đây chúng ta chỉ cung cấp cho nó chiều cao (height). Vì vậy, nó hoạt động như một widget cung cấp không gian theo chiều dọc. Chúng ta sử dụng chúng để thêm khoảng cách giữa các widget.

CircleAvatar Widget

Widget này tạo thành một vòng tròn với kích thước do chúng ta xác định. Nó cần các tham số như radius, backgroundColor, backgroundImage, child, v.v. Ở đây chúng ta đang sử dụng hai CircleAvatar widget. Một cái giữ hình ảnh hồ sơ và có bán kính nhỏ hơn hình ảnh gốc của nó. Widget có bán kính lớn hơn một chút để nó trải rộng ra và tạo hiệu ứng đường viền tròn có màu xanh lam.

Text Widget

Text Widget được sử dụng để hiển thị một số text với style riêng của chúng ta. Chúng ta dùng widget này để hiển thị tất cả các văn bản của chúng ta với các kích thước phông chữ, trọng lượng (weight) phông chữ và màu sắc khác nhau.

RaisedButton Widget

Như tên của nó, các widget RaisedButton cung cấp cho chúng ta một button được build sẵn. Nó xuất hiện trên màn hình bằng với một shadow (đổ bóng). Chúng ta có thể cung cấp một child widget cho nó. Trong trường hợp này, ta đang cung cấp cho nó một ContainerRow là child của nó. Row cũng chứa hai widget lồng nhau là Icon widget và Text widget.

Chúng ta cũng có thể gán bất kỳ chức năng nào mà chúng ta muốn RaisedButton thực thi bằng cách sử dụng thuộc tính onPressed. Trong trường hợp này, ta sử dụng hàm _launchNativeApps(...). Chức năng này thực hiện mỗi khi nút được nhấn.

Function _launchNativeApps()

Trong hàm này, chúng ta sử dụng một hàm có tên canLaunch(...) được cung cấp bởi url_launcher package mà chúng ta đã cài đặt trước đó. Đây là một chức năng bất đồng bộ để kiểm tra xem liệu url được cung cấp có thể chạy được trong ứng dụng tương thích khác hay không. Nếu một ứng dụng như vậy đang có trong thiết bị thì ứng dụng đó được khởi động với các giá trị được xác định trước của chúng ta trong url và nếu không có thì nó sẽ gây ra lỗi.

Chia app thành các widget nhỏ hơn

Hiện tại, đây là một single page app nhưng hãy tưởng tượng nếu chúng ta bao gồm routing, multiple screens, navigation drawer/ bottom nav bar, v.v. Nó sẽ rất lớn và việc viết tất cả những thứ đó trong một file duy nhất sẽ là điều kinh khủng. Ngoài ra, việc maintain và debug code cũng sẽ là một cơn ác mộng!

Để khắc phục vấn đề này, chúng ta có thể chia ứng dụng thành các widget riêng biệt. Trong đoạn code trên, chúng ta có thể thấy rõ ràng rằng 3 nút có code gần như giống nhau và điều duy nhất khác biệt là url của chúng. Vì vậy, những gì chúng ta có thể làm là tạo một widget riêng cho nút và sử dụng chúng ba lần thay vì viết cùng một đoạn code ba lần.

Vì vậy, chúng ta sẽ tạo một file mới trong thư mục lib có tên là custom_button.dart. Trong file này, chúng ta sẽ import material.dart như thường lệ và tạo một StatelessWidget mới để chuyển tất cả code liên quan đến Button vào trong file này. Nó sẽ trông giống thế này:

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

class CustomButton extends StatelessWidget {
  final String uri;
  final IconData icon;
  final String buttonText;
  CustomButton({this.uri, this.icon, this.buttonText});

  _launchNativeApps(String url) async {
    if (await canLaunch(url)) {
      await launch(url);
    } else {
      throw 'Could not launch $url';
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 30),
      child: RaisedButton(
        onPressed: () {
          _launchNativeApps(uri);
        },
        child: Container(
          height: 40,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Icon(
                icon,
                size: 30,
                color: Colors.blue[800],
              ),
              Text(
                buttonText,
                style: TextStyle(
                    fontWeight: FontWeight.w600,
                    fontSize: 15,
                    color: Colors.grey[700]),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Ở đây, chúng ta đã chuyển function _launchNativeApps (..) {...} qua từ file main.dart. Ngoài ra, chúng ta khởi tạo widget này với tất cả các tham số như uri, iconbuttonText đến widget này để hiển thị các nội dung khác nhau.

Bạn sẽ nhận thấy rằng mình đã sử dụng {..} trong hàm khởi tạo CustomButton({this.uri, this.icon, this.buttonText});. Đây là cú pháp hàm dựng rút gọn mà dart cung cấp cho chúng ta. {..} làm cho điều này dễ dàng hơn khi widget phải chuyển các giá trị các biến tương ứng của chúng. Ví dụ: nếu chúng ta sử dụng CustomButton widget của mình ở bất kỳ đâu thì nó như thế này:

CustomButton(
         uri: 'https://shashankbiplav.com',
         icon: Icons.person_outline_outlined,
         buttonText: 'Website',
),

Với điều này, lợi thế là hàm dựng sẽ rõ ràng hơn và ta không phải nhớ thứ tự của các biến cần truyền vào nữa.

Chúng ta cũng có thể tạo một Widget riêng cho ảnh đại diện và chúng ta sẽ đặt tên file là profile_image.dart trong thư mục lib. Chúng ta sẽ di chuyển tất cả code liên quan đến hình ảnh của chúng ta ở đây vào StatelessWidget mới này. Nó sẽ trông giống thế này:

import 'package:flutter/material.dart';

class ProfileImage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CircleAvatar(
      radius: 100,
      backgroundColor: Theme.of(context).primaryColor,
      child: CircleAvatar(
        radius: 95,
        backgroundColor: Colors.white70,
        backgroundImage: NetworkImage('https://i.ibb.co/n3RzK2L/shashank.jpg'),
      ),
    );
  }
}

Giờ đây, file main.dart của chúng ta đã gọn gàng hơn, dễ bảo trì và debug hơn nhiều. Ngoài ra, chúng ta đã nâng cao đáng kể khả năng đọc và khả năng tái sử dụng code:

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

import './custom_button.dart';
import './profile_image.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Portfolio App',
      theme: ThemeData(
        primarySwatch: Colors.blueGrey,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Scaffold(
        backgroundColor: Color.fromRGBO(227, 234, 237, 1),
        appBar: AppBar(
          title: Text(
            'Portfolio',
          ),
          centerTitle: true,
        ),
        body: SingleChildScrollView(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              SizedBox(height: 30),
              ProfileImage(),
              SizedBox(height: 20),
              Text(
                'Shashank Biplav',
                style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700),
              ),
              Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
                child: Divider(),
              ),
              Text(
                'SOFTWARE ENGINEER & TECH BLOGGER',
                style: TextStyle(fontSize: 19, fontWeight: FontWeight.w700),
              ),
              Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 20, vertical: 0),
                child: Divider(),
              ),
              Padding(
                padding: const EdgeInsets.all(30.0),
                child: Text(
                  'Hello, I am Shashank Biplav a Full-Stack developer based in India specializing in building apps for Mobile and the Web.',
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 18),
                ),
              ),
              CustomButton(
                uri: 'https://shashankbiplav.com',
                icon: Icons.person_outline_outlined,
                buttonText: 'Website',
              ),
              SizedBox(height: 20),
              CustomButton(
                uri: 'tel:+917004026852',
                icon: Icons.wifi_calling_rounded,
                buttonText: '+91- 700-402-6852',
              ),
              SizedBox(height: 20),

              CustomButton(
                uri:'mailto:[email protected]?subject=I would like to build an app with you!&body=Hi there,n I have an awesome app idea,nn',
                icon: Icons.email_rounded,
                buttonText: '[email protected]',
              ),
              SizedBox(height: 20),
            ],
          ),
        ),
      ),
    );
  }
}

Kết

Giờ đây, bạn chỉ cần thay thế tất cả các url trong code thành của bạn và bạn sẽ có một Portfolio App tuyệt vời. Tôi hy vọng bạn sẽ thích bài viết này.

Bài viết được lược dịch từ Shashank Biplav.

Nguồn: viblo.asia

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ