Flutter cơ bản cho iOS developer (phần 3)

Phần 1: Views

https://viblo.asia/p/flutter-co-ban-cho-ios-developer-phan-1-Az45bQ6olxY

Phần 2: Navigation, Thread & Xử lý bất đồng bộ, Cấu trúc project & Assets

https://viblo.asia/p/flutter-co-ban-cho-ios-developer-phan-2-Az45bQ9LlxY

ViewController

ViewController trong Flutter

Như chúng ta đã biết, trong iOS, ViewController đại diện cho toàn bộ hoặc một phần giao diện hiển thị trên màn hình. Các ViewController có thể được kết hợp với nhau để xây dựng nên các giao diện phức tạp và "scale up" quy mô ứng dụng.

Còn với Flutter, tương ứng với các ViewController là các Widget. Như đã đề cập từ phần trước, trong Flutter, tất cả mọi thứ đều là widget. Chúng ta sử dụng class Navigator để di chuyển qua lại giữa các Route. Mỗi Route thể hiện một màn hình khác nhau, ở đó các Widget biểu diễn các trạng thái khác nhau của màn hình, có thể render lại khi gọi hàm setState().

Lifecycle event

Trong iOS, chúng ta có thể override các method của ViewController như: viewDidLoad(), viewDidAppear(animated:), viewDidDisapear(animated:)... để capture, tracking các lifecycle, trạng thái của các view.

Còn để observe lifecycle của toàn app thì sử dụng các method trong AppDelegate.

Flutter không có khái niệm tương ứng giống hoàn toàn, nhưng chúng ta vẫn có thể tracking các lifecycle event bằng cách sử dụng observer WidgetsBinding qua function didChangeAppLifecycleState().

Các lifecycle event có thể observe được là:

  • inactive - Ứng dụng ở trạng thái inactive, không thể nhận input từ user. Event này chỉ có trong iOS còn Android thì không.
  • paused - Ứng dụng hiện đang không hiển thị lên cho user, không thể nhận input từ user, nhưng vẫn đang chạy ngầm dưới background.
  • resumed - Ứng dụng hiện lên và có thể nhận input từ user.
  • suspending - Ứng dụng tạm thời bị tạm dừng trong giây lát. iOS không có event này.

Để biết thêm chi tiết ý nghĩa của các state trên, có thể tham khảo tài liệu về AppLifecycleStatus.

Layout

UITableView và UICollectionView trong Flutter

Trong iOS, để hiển thị dữ liệu dạng list theo chiều dọc màn hình, chúng ta sử dụng UITableView, còn hiển thị theo cả 2 chiều màn hình dùng UICollectionView.

Tương ứng với UITableView và UICollectionView trong Flutter là widget ListView.

Trong iOS, các view này có các method delegate để chỉ định số lượng row, từng view cell cho từng index path, size của từng cell... Còn với Flutter, ta chỉ cần truyền vào list các widget con trong ListView, Flutter sẽ lo phần còn lại.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: _getListData()),
    );
  }

  _getListData() {
    List<Widget> widgets = [];
    // Generate 100 widget Text với Padding
    for (int i = 0; i < 100; i++) {
      widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i")));
    }
    return widgets;
  }
}

Handle event khi click vào một item

Với UITableView, UICollectionView, chúng ta phải implement method tableView:didSelectRowAtIndexPath: hoặc collectionView:didSelectRowAtIndexPath:. Nhưng trong Flutter, chỉ cần handle touch event của widget đã truyền vào.

  _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(GestureDetector(
        child: Padding(
          padding: EdgeInsets.all(10.0),
          child: Text("Row $i"),
        ),
        onTap: () {
          print('row tapped');
        },
      ));
    }
    return widgets;
  }
}

Cách reload ListView

Trong iOS, khi update data, chúng ta báo cho table view hoặc collection view reload, render lại bằng method reloadData().

Trong Flutter, nếu update list widget trong method setState(), chúng ta có thể thấy rằng giao diện sẽ chẳng có gì thay đổi. Điều này bởi vì khi setState() được gọi, Flutter rendering engine sẽ nhìn vào widget tree để tìm kiếm những widget thay đổi, từ đó sẽ render lại. Nhưng khi gặp widget ListView, Flutter sẽ thực hiện một action so sánh == và xác định rằng hai ListView trước và sau update state giống nhau. Và không có gì thay đổi, không cần update và render lại.

Để update ListView một cách đơn giản đó là tạo một List mới bên trong setState() và copy data từ list cũ sang list mới. Mặc dù cách này đơn giản nhưng không nên dùng trong trường hợp data set lớn, như ví dụ tiếp theo dưới đây.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: widgets),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text("Row $i"),
      ),
      onTap: () {
        setState(() {
          widgets = List.from(widgets);
          widgets.add(getRow(widgets.length + 1));
          print('row $i');
        });
      },
    );
  }
}

Cách hiệu quả và được khuyên dùng để reload ListView đó là sử dụng ListView.builder(). Cách này vẫn hoạt động hiệu quả ngay cả khi bạn có một data set lớn.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView.builder(
        itemCount: widgets.length,
        itemBuilder: (BuildContext context, int position) {
          return getRow(position);
        },
      ),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text("Row $i"),
      ),
      onTap: () {
        setState(() {
          widgets.add(getRow(widgets.length + 1));
          print('row $i');
        });
      },
    );
  }
}

Thay vì tạo mới một ListView, sử dụng ListView.builder sẽ cần 2 param: số lượng item trong list và một ItemBuilder function.

ItemBuilder function gần giống mới method delegate cellForItemAt của table view, collection view trong iOS. Và ở function onTap, chúng ta không cần phải tạo lại list nữa, thay vào đó chỉ cần add item là đủ.

UIScrollView trong Flutter

UIScrollView trong iOS dùng để hiển thị các view, nội dung có kích thước vượt quá kích thước màn hình thiết bị.

Trong Flutter, cách dễ nhất vẫn là sử dụng ListView để làm điều tương tự. ListView đóng vai trò giống cả UIScrollViewUITableView trong iOS, cho phép hiển thị UI lớn theo chiều dọc màn hình.


Widget build(BuildContext context) {
  return ListView(
    children: <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}

GestureRecognizer và handle touch event

Cách thêm click listener vào widget

Trong iOS, chúng ta sử dụng UIGestureRecognizer để handle các event click, double click... cho các view. Trong Flutter, có 2 cách để thêm các touch listener:

  1. Nếu widget hỗ trợ detect các touch, chỉ cần thêm function và thêm code để handle. Ví dụ, widget RaisedButton có sẵn param onPressed để handle khi tap vào:

Widget build(BuildContext context) {
  return RaisedButton(
    onPressed: () {
      print("click");
    },
    child: Text("Button"),
  );
}
  1. Nếu widget không hỗ trợ các function giúp detect các event thì ta có thể wrap chúng vào widget cha GestureDetector và sử dụng các function như onTap...
class SampleApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          child: FlutterLogo(
            size: 200.0,
          ),
          onTap: () {
            print("tap");
          },
        ),
      ),
    );
  }
}

Handle các event touch khác

Sử dụng GestureDetector, chúng ta có thể handle rất nhiều các touch event khác nhau như:

Single tap

  • onTapDown - Event khi user bắt đầu tap vào màn hình.
  • onTapUp - Event khi user thả tay, dừng tap vào màn hình.
  • onTap - Event chung khi user tap vào màn hình.
  • onTapCancel - Event khi tap event bị cancel bởi user.

Double tap

  • onDoubleTap - Event khi user tap liên tục tại 2 điểm trên màn hình trong một khoảng thời gian ngắn.

Long press

  • onLongPress - Event khi user nhấn giữ tap trong một khoảng thời gian.

Vertical drag

  • onVerticalDragStart - Event khi user bắt đầu drag theo chiều dọc màn hình.
  • onVerticalDragUpdate - Event khi user tiếp tục drag theo chiều dọc màn hình.
  • onVerticalDragEnd - Event khi user kết thúc thao tác drag theo chiều dọc màn hình.

Horizontal drag

  • onHorizontalDragStart - Event khi user bắt đầu drag theo chiều ngang màn hình.
  • onHorizontalDragUpdate - Event khi user tiếp tục drag theo chiều ngang màn hình.
  • onHorizontalDragEnd - Event khi user kết thúc thao tác drag theo chiều ngang màn hình.

Ví dụ dưới đây handle event khi double tap vào một logo sử dụng GestureDetector:

AnimationController controller;
CurvedAnimation curve;


void initState() {
  controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
  curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}

class SampleApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          child: RotationTransition(
            turns: curve,
            child: FlutterLogo(
              size: 200.0,
            )),
          onDoubleTap: () {
            if (controller.isCompleted) {
              controller.reverse();
            } else {
              controller.forward();
            }
          },
        ),
      ),
    );
  }
}