Flutter

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

Phần 1: Views

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

Trong phần 2 này, chúng ta sẽ tìm hiểu về chủ đề Navigation, Thread & Xử lý bất đồng bộ, Cấu trúc project && Assets trong Flutter từ góc độ của một iOS developer.

Navigation

Cách navigate giữa các màn hình

Trong iOS, chúng ta có thể sử dụng UINavigationController để navigate qua lại giữa các màn hình bằng việc lưu các view controller vào stack.

Để hiển thị một màn hình mới, chỉ cần push view controller đó và thêm vào stack.

Tương tự, để quay lại màn hình trước đó, chỉ cần pop view controller tương ứng ra khỏi stack.

Flutter có cách giải quyết tương tự, đó là sử dụng NavigatorRoutes. Một Route mô tả "đường dẫn" đại diện cho một màn hình hoặc một trang trong app.

Còn Navigatior là một loại widget đặc biệt dùng để quản lý các Route. Route có thể hiểu gần giống như UIViewControllerNavigator thì tương tự như UINavigationController của iOS vậy.

Chúng ta vẫn có thể chuyển hướng hoặc quay về màn hình trước thông qua các function push()pop().

Để navigate giữa các màn hình, chúng ta có thể dùng các cách sau:

  • Mô tả qua một Map các tên route. (Sử dụng trong MaterialApp)
  • Trực tiếp navigate đến route đó. (Sử dụng trong WidgetApp)

Ví dụ sau mô tả cách thứ nhất: sử dụng Map các route.

void main() {
  runApp(MaterialApp(
    home: MyAppHome(),
    routes: <String, WidgetBuilder> {
      '/a': (BuildContext context) => MyPage(title: 'Screen A'),
      '/b': (BuildContext context) => MyPage(title: 'Screen B'),
      '/c': (BuildContext context) => MyPage(title: 'Screen C'),
    },
  ));
}

Navigate đến một route bằng cách truyền tên route đó vào function pushNamed() của Navigator.

Navigator.of(context).pushNamed('/b');

Class Navigator xử lý các route và còn được dùng để nhận về kết quả trả về từ route mà đã được push vào stack sau khi nó bị pop.

Việc này được thực hiện asynchronous qua keyword await của Future.

Ví dụ, để navigate đến một màn hình cho phép user chọn vị trí trên bản đồ chẳng hạn, push đến route "location" như sau:

Map coordinates = await Navigator.of(context).pushNamed('/location');

Sau khi user chọn xong vị trí trên bản đồ, ta quay lại màn hình trước bằng function pop() và truyền theo kết quả location đã chọn:

Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});

Khi đó, ta sẽ nhận được giá trị trả về từ màn hình vừa bị pop thông qua variable Map coordinates.

Cách khởi động external app

Trong iOS, khi cần chuyển đến một app khác app hiện tại, chúng ta sử dụng action scheme.

Để làm được điều tương tự trong Flutter, chúng ta có thể sử dụng các plugin có sẵn như là url_launcher.

Tuy nhiên url_launcher cũng khá hạn chế, chỉ có thể thực hiện một số action đơn giản như: mở browser, gửi email, mở app nhắn tin, gọi điện của hệ thống...

Cách pop về iOS native view controller

Khi gọi function SystemNavigator.pop() từ Flutter Dart code sẽ thực hiện đoạn code iOS tương ứng sau:

if let navigationController = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController {
    navigationController.popViewController(animated: false)
}

Bạn có thể tìm hiểu thêm về platform channel để có thể gọi các đoạn code iOS tuỳ ý.

Threading & Xử lý bất đồng bộ (asynchronous)

Cách viết code asynchronous

Ngôn ngữ Dart sử dụng mô hình excute kiểu single-thread với sự hỗ trợ của Isolate (phương pháp excute code trên một thread khác) cùng với event loop.

Nếu không dùng Isolate, code Dart của bạn sẽ được excute trên thread main UI và được điều tiết bởi event loop, khá giống với iOS main loop.

Để tránh tình trạng các task nặng, tốn thời gian chạy trên main thread làm treo UI, chúng ta sử dụng keyword async/await để thực hiện các task không đồng bộ.

Ví dụ, đoạn code thực hiện task tốn nhiều thời gian load từ mạng internet dưới đây sẽ không làm UI bị treo vì phần task nặng đã được thực hiện trên một thread khác.

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

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();

    loadData();
  }

  
  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 Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}")
    );
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

Sau khi lời gọi mạng được chờ thực hiện xong với keyword await, UI sẽ được update qua function setState().

Dữ liệu sau khi get về sẽ được hiển thị lên ListView.

Cách tạo request mạng

Trong Flutter, package http là một package khá nổi tiếng, giúp tạo các request mạng hết sức đơn giản và dễ dàng.

Để dùng package http, chỉ cần thêm nó vào list dependencyy trong file pubspec.yaml:

dependencies:
  ...
  http: ^0.11.3+16

Để tạo request mạng, thêm keyword async vào sau function và keyword await vào trước function http.get()

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

Hiển thị progress của một task background

Trong iOS, chúng ta thường sử dụng UIActivityIndicatorView hoặc UIProgressView để hiển thị progress của một task ngốn thời gian chạy dưới background.

Flutter có widget ProgressIndicator, hiển thị ra khi task bắt đầu và ẩn đi khi kết thúc, điều khiển qua một flag kiểu boolean.

Trong ví dụ dưới đây, function build được chia ra làm 3 function khác nhau. Nếu showLoadingDialog() bằng true (khi widgets.length == 0) thì sẽ hiển thị ProgressIndicator. Ngược lại, ẩn loading indicator và hiển thị ListView với dữ liệu trả về khi request mạng kết thúc.

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

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();
    loadData();
  }

  showLoadingDialog() {
    return widgets.length == 0;
  }

  // Generate body của widget tùy theo boolean flag showLoadingDialog()
  getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  // Generate loading indicator
  getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

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

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });
      
  // Function generate các row của ListView
  Widget getRow(int i) {
    return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));
  }

  // Function load data asynchronous từ mạng
  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    // Set lại state, reload toàn bộ widget
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

Project structure, localization, dependencies and assets

Cấu trúc project, localization, dependencies và assets

Image assets trong Flutter, multiple resolutions

Trong iOS, ảnh và assets có thể khác nhau. Nhưng trong Flutter, chỉ có khái niệm assets. iOS, các resource được lưu trong thư mục Images.xcasset, còn Flutter thì được lưu trong thư mục assets.

Assets không chỉ là ảnh mà còn có thể là file json, video, file âm thanh...

Ví dụ, ta có file data.json được lưu trong thư mục assets.

my-assets/data.json

Thì sẽ được khai báo trong file pubspec.yaml như sau:

assets:
 - my-assets/data.json

Và được gọi sử dụng trong code thông qua AssetBundle:

import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;

Future<String> loadAsset() async {
  return await rootBundle.loadString('my-assets/data.json');
}

Đối với ảnh, Flutter giống với iOS cũng chia ảnh theo mật độ pixel (ảnh 1x, 2x, 3x...) được thể hiện qua thuộc tính devicePixelRatio tỉ lệ scale ảnh.

Ví dụ, để thêm ảnh tên là my_icon.png vào project Flutter, ta chỉ cần lưu chúng vào lưu chúng vào thư mục images.

Ảnh 1x được lưu trực tiếp còn ảnh 2x, 3x thì lưu trong các thư mục con 2.0x, 3.0x.

images/my_icon.png       // Base: 1.0x image
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image

Tiếp theo, khai báo trong file pubspec.yaml:

assets:
 - images/my_icon.png

Để truy cập ảnh vừa thêm, sử dụng AssetImage:

return AssetImage("images/my_icon.png");

Hoặc sử dụng trực tiếp qua widget Image:


Widget build(BuildContext context) {
  return Image.asset("images/my_image.png");
}

Để tìm hiểu chi tiết hơn, xem thêm Adding Assets and Images in Flutter.

Cách lưu các strings, xử lý localization

Không giống như iOS, có các file Localizable.strings để lưu các string và localization theo từng ngôn ngữ rất dễ dàng và tiện lợi, hiện tại Flutter chưa có một cơ chế chính thức nào hỗ trợ việc này cả.

Cách tốt nhất để lưu các constant string là lưu chúng làm các static field của một class nào đó. Chẳng hạn:

class Strings {
  static String welcomeMessage = "Welcome To Flutter";
}

Truy cập string đã lưu bằng:

Text(Strings.welcomeMessage)

Mặc định, Flutter chỉ hỗ trợ tiếng Anh - Mỹ cho các string. Nếu muốn localize hóa các string, chúng ta phải sử dụng package flutter_localizations.

Ngoài ra cũng có thể sử dụng thêm package intl của Dart để localize các thông tin như định dạng ngày giờ, tiền tệ...

dependencies:
  # ...
  flutter_localizations:
    sdk: flutter
  intl: "^0.15.6"

Để sử dụng được package flutter_localizations, thêm property localizationsDelegateslocalizationsDelegates vào app widget.

import 'package:flutter_localizations/flutter_localizations.dart';

MaterialApp(
 localizationsDelegates: [
   GlobalMaterialLocalizations.delegate,
   GlobalWidgetsLocalizations.delegate,
 ],
 supportedLocales: [
    const Locale('en', 'US'), // English
    const Locale('vi', 'VN'), // Vietnamese
    // ...
  ],
  // ...
)

CocoaPods trong Flutter?

Trong iOS, các lib được khai báo trong file Podfile. Còn Flutter sử dụng Dart build system và Pub package manager để quản lý các dependency.

Vì vậy, các lib trong Flutter được khai báo trong một file pubspec.yaml, tương tự Podfile trong iOS. Chúng ta có thể tìm được nhiều lib hay ho trong Flutter từ trang list các package của Pub.

Registration Login
Sign in with social account
or
Lost your Password?
Registration Login
Sign in with social account
or
A password will be send on your post
Registration Login
Registration