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

Flutter official docs: https://flutter.io/docs/get-started/flutter-for/ios-devs

Flutter là một UI mobile framework đang khá nổi tiếng của Google, giúp phát triển các ứng dụng cross-platform trên cả hai nền tảng iOS và Android từ một source duy nhất.

Trong bài viết này, chúng ta - những developer iOS đơn thuần sẽ cùng nhau so sánh Flutter với iOS và tìm hiểu cách áp dụng những kiến thức đã biết trong lập trình iOS để bắt đầu học Flutter.

Views

UIView tương ứng với cái gì trong Flutter?

Trong iOS, phần lớn UI được tạo nên từ các view object - instance của class UIView. Những view object này còn có thể chứa nhiều view con khác, định hình lên giao diện.

Còn trong Flutter, tương ứng với UIView của iOS, chúng ta có các Widget. Widget không hoàn toàn giống với UIView nhưng khi bắt đầu tìm hiểu Flutter, hãy cứ nghĩ đơn giản: "Widget là cách để bạn định nghĩa và tạo ra giao diện người dùng".

Một số điểm khác biệt giữa Widget và UIView có thể kể đến như:

Các widget có lifespan (tuổi thọ) và cách chúng tồn tại trong bộ nhớ cũng khác. Các widget immutable (không thể thay đổi), nghĩa là sau khi được tạo ra, chúng ta không thể update trực tiếp widget đó.

Ví dụ ta không thể đổi màu background của một widget bằng cách gọi function updateBackgroundColor() nào đó. Khi cần update hoặc thay đổi state của widget, Flutter sẽ tạo lại mới hoàn toàn một loạt các instance tree các widget liên quan.

Còn trong iOS, một view chỉ được tạo một lần, khi update sẽ không tạo lại instance mới trong bộ nhớ. View sẽ được re-draw, cập nhật UI mới khi function setNeedsDisplay() được gọi.

Hơn nữa, không giống với UIView, các widget của Flutter "lightweight", một phần là do tính "không thể thay đổi" (immutability) của chúng. Bởi vì bản thân các widget không hẳn là các view, không phải chịu trách nhiệm draw UI lên màn hình. Widget chỉ là các đối tượng mô tả view, là bản thiết kế của các view.

Flutter có thư viện các Material Components, là các widget được thiết kế theo phong cách Material Design. Material Design là một hệ thống các UI library chung được thiết kế cho tất cả các mobile platform, bao gồm cả iOS và Android.

Ngoài ra, Flutter cũng hỗ trợ các Cupertino widget giúp tạo ra các UI mang phong cách đặc trưng giống hệt của iOS.

Ví dụ:

Cupertino Action Sheet

Cupertino Switch

Làm thế nào để update các Widget?

Để update các view trong iOS, chúng ta có thể trực tiếp thay đổi chúng bằng việc sử dụng các property hoặc function. Nhưng trong Flutter, các widget không thể thay đổi và không thể update một cách trực tiếp. Thay vào đó, chúng ta phải update các widget thông qua state của chúng.

Điều này dẫn đến sự ra đời của hai khái niệm StatefulWidgetStatelessWidget. StatelessWidget - như tên gọi của nó, không lưu giữ và quản lý theo state.

StatelessWidget thường được sử dụng để xây dựng các UI tĩnh, không thay đổi, chỉ phụ thuộc vào các thông tin khởi tạo ban đầu.

Ví dụ, để hiển thị một image cố định cho trước, trong iOS chúng ta dùng UIImageView với property image. Với trường hợp này trong Flutter, ta nên dùng StatelessWidget.

Còn nếu bạn muốn cập nhật UI một cách linh động, ví dụ tuỳ theo data trả về từ server sau khi request HTTP chẳng hạn thì cần phải dùng đến StatefulWidget. Sau khi request thành công, ta chỉ cần báo cho Flutter biết State của widget cần update đã được thay đổi. Flutter sẽ tự động re-draw widget đó.

Điểm khác biệt lớn nhất giữa StatelessWidget và StatefulWidget là các StatefulWidget có một object kiểu State dùng để lưu giữ state, data, property của widget. Các thông tin này không bị mất đi khi Flutter update, rebuild widget.

Nếu bạn còn thấy mơ hồ, hãy nhớ quy tắc đơn giản này: nếu một widget thay đổi bên ngoài method build (do tương tác với user chẳng hạn), thì nó là stateful. Còn nếu widget được build một lần duy nhất và không bao giờ thay đổi thì nó là stateless. Tuy nhiên, nếu một widget thuộc kiểu stateful thì widget cha của nó vẫn có thể là một stateless widget nếu bản thân widget đó không re-act với những thay đổi đó.

Ví dụ sau đây thể hiện cách sử dụng một StatelessWidget. Một StatelessWidget thường gặp đó là Text widget. Nếu xem phần code implement của Text, dễ dàng có thể thấy được Text được subclass từ StatelessWidget.

Text(
  'I like Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
);

Nhìn vào đoạn code trên, chúng ta có thể thấy Text widget không chứa state nào. Nó được render dựa trên các parameter được truyền vào constructor. Ngoài ra không có gì thêm.

Tuy nhiên, nếu muốn thay đổi text của label, ví dụ khi tap vào một FloatingActionButton thì làm như thế nào?

Để làm được điều này, chỉ cần wrap Text widget vào một StatefulWidget khác và update nó khi user tap vào button.

Ví dụ:

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  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);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {

  String textToShow = "I Like Flutter";
  void _updateText() {
    setState(() {
      textToShow = "Flutter is Awesome!";
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(child: Text(textToShow)),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

Làm thế nào để xây dựng giao diện? Interface Builder, Storyboad?

Trong lập trình iOS, chúng ta thường sử dụng Interface Builder (các storyboard, các file xib) để xây dựng, tổ chức các view, set các constraint trực tiếp, trực quan ngay trên giao diện preview. Hoặc cũng có thể thêm các constraint bằng code khi run time.

Tuy nhiên, với Flutter lại không có các công cụ giúp kéo thả giao diện đơn giản như trong iOS hoặc Android. Thay vào đó, bạn sẽ phải code tất cả UI bằng tay. Tuy nhiên, Flutter có tính năng "hot reload" giúp bạn chỉ cần save code là UI trên simulator sẽ ngay lập tức cập nhật giao diện theo code vừa sửa mà không cần rebuild như iOS hoặc Android.

Các widget trong Flutter được tổ chức thành dạng cây. Một widget có thể chứa một hoặc nhiều widget "child".

Để thêm khoảng cách giữa các widget, chúng ta có thể dùng thuộc tính padding như ví dụ dưới đây:


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: CupertinoButton(
        onPressed: () {
          setState(() { _pressedCount += 1; });
        },
        child: Text('Hello'),
        padding: EdgeInsets.only(left: 10.0, right: 10.0),
      ),
    ),
  );
}

Ngoài ra Flutter còn cung cấp nhiều cách khác nhau giúp lay out các widget rất linh động. Chi tiết tham khảo widget catalog

Làm sao để add/remove một component khỏi UI layout?

Trong iOS, để thêm child view vào một view, ta gọi function addSubView(). Còn muốn xoá một view ra khỏi superview của nó thì dùng function removeFromSuperView().

Nhưng trong Flutter, các widget không để thay đổi nên không thể sử dụng function add/remove như trong iOS.

Để làm được điều tương tự, chúng ta phải sử dụng một biến boolean làm flag để truyền vào property child widget.

Ví dụ:

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  
  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> {
  // Default value for toggle
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  _getToggleChild() {
    if (toggle) {
      return Text('Toggle One');
    } else {
      return CupertinoButton(
        onPressed: () {},
        child: Text('Toggle Two'),
      );
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: _getToggleChild(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _toggle,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

Làm sao để vẽ lên màn hình?

Trong iOS, chúng ta sử dụng các class của CoreGraphics để draw line và shape lên giao diện. Flutter thì có một API chuyên về đồ hoạ dựa trên class Canvas, cùng với hai class khác hỗ trợ vẽ: CustomPaintCustomPainter.

Ví dụ sau đây tạo ra một widget cho phép user vẽ chữ ký trên màn hình. Sử dụng onPanUpdateonPanEnd để draw các điểm mà user vuốt đến.

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);

  final List<Offset> points; // List point cần vẽ

  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.black // Set màu cho painter
      ..strokeCap = StrokeCap.round // Set kiểu nét
      ..strokeWidth = 5.0; // Set độ dày net
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i], points[i + 1], paint); // Draw line đến các point
    }
  }

  bool shouldRepaint(SignaturePainter other) => other.points != points;
}

class Signature extends StatefulWidget {
  SignatureState createState() => SignatureState();
}

class SignatureState extends State<Signature> {

  List<Offset> _points = <Offset>[];

  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox referenceBox = context.findRenderObject();
          Offset localPosition =
          referenceBox.globalToLocal(details.globalPosition);
          _points = List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite),
    );
  }
}

Làm sao để set widget opacity?

Trong iOS, đơn giản chỉ cần thay đổi property .opacity hoặc .alpha là xong.

Trong Flutter thì phải wrap widget cần set opacity vào widget Opacity.

Làm sao để customize widget?

Trong iOS, chúng ta đơn giản chỉ cần subclass UIView hoặc sử dụng các control có sẵn rồi override, implement các method cần thiết để tạo ra một custom view theo ý muốn.

Còn với Flutter, ta cần phải kết hợp các widget thành phần (thay vì extend).

Ví dụ, để tạo một custom button từ RaisedButton với một title Label:

class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

  
  Widget build(BuildContext context) {
    return RaisedButton(onPressed: () {}, child: Text(label));
  }
}

Và có thể sử dụng widget CustomButton bình thường như bao widget thông thường khác.


Widget build(BuildContext context) {
  return Center(
    child: CustomButton("Hello"),
  );
}