Flutter

Flutter, Bài 5: Phân tích ứng dụng “cho sẵn” của Flutter

Và bây giờ, chúng ta bắt đầu tìm hiểu về ứng dụng Flutter mặc định được cho sẵn để từ đó bạn sẽ hiểu được nguyên tắc hoạt động của một app được viết bằng Flutter. Cá nhân tôi thấy việc đi từ đây giúp tôi dễ hiểu về Flutter hơn so với cách đi từ lí thuyết thuần với đầy chữ nghĩa, mà ngay cả bản thân doc chính chủ của Flutter giải thích không mấy rõ ràng, mà nếu đem so với trang Android Developers thì Flutter còn thua kém rất xa.

Việc debug trên Android Studio với Flutter không diễn ra theo hướng bình thường, vì một là việc build ứng dụng Flutter dưới dạng APK hay IPA không do Gradle làm, mà do Flutter bin trong bộ FlutterSDK đảm nhận, hai là nó còn hỗ trợ debug trên thiết bị iOS nữa (với iOS Simulator trên macOS). Vì vậy, trước hết bạn sẽ tự bật máy ảo Android từ nơi có dòng <no devices>, rồi sau đó mới nhấn nút tam giác màu xanh như hình minh họa dưới đây:

Đầu tiên, bạn sẽ để ý chúng ta bắt đầu code từ lib/main.dart, và mọi phần code khởi chạy ứng dụng của chúng ta sẽ bắt đầu từ đó. Nhưng trước hết, bạn sẽ cần tạm bỏ đi các khái niệm về Activity với Android hay ViewController với iOS. Thực tế, Flutter không có hành động “chuyển màn hình” thực sự, tức sang Activity mới như trên Android hay sang ViewController mới như iOS, mà tất cả chỉ là việc một Widget mới toàn màn hình sẽ nằm “đè” lên Widget hiện tại để gây cảm giác cho người dùng mà thôi. Và bây giờ, bạn sẽ thấy tập tin main.dart của bạn tương tự như bên dưới, chỉ khác là tôi đã loại bỏ hầu hết các phần comment để bạn dễ quan sát hơn:

Đầu tiên, bạn sẽ để ý thấy hàm main(). Có thể bạn đang thắc mắc, thót mót, thúc múc rằng trong Android đâu có hàm Java#main? Sao tên này lại có? Tôi giải thích đây. Nếu bạn đã theo dõi EITGUIDE được một thời gian lâu lâu rồi, có thể bạn sẽ nhớ tới việc tôi đã giải thích là hàm Java#main do một tên “vô hình” được gán danh là Zygote lo liệu (trong bài về Fragment). Tên Zygote này là của hệ thống Android, và mặc định khi bạn hay người dùng Android nào click mở một ứng dụng, thì đầu tiên, hệ thống Android sẽ tìm tới package của app đó, gọi class Zygote và yêu cầu Zygote chạy hàm main. Nội dung của hàm main này chỉ chứa đúng những phần code trỏ tới class Application, và class Activity có intent-filter là MAIN/LAUNCHER (là cái người dùng click vào) và rồi Activity đó sẽ được gọi lên. Đó là nguyên tắc hoạt động của ứng dụng Android với code Java thuần, và nhiệm vụ của bạn khi làm việc với Activity hay Service là bắt đầu override Activity#onCreate, hay Service#onStartCommandService#onBind. Code hàm main đã được lo liệu sẵn.

Nhưng với Flutter thì khác. Bản chất của Flutter là nó render giao diện với bộ engine của nó. Nó chỉ “mượn” phần code khởi động của hệ thống để được bật lên thôi, còn mọi thứ khác là do bản thân nó tự thực hiện. Điều này làm Flutter có tính độc lập và dễ thao tác với cả AndroidiOS hơn React Native vốn chuyển đổi các <React.Component Widget> thành các Android View hay iOS UIView trên hệ thống đích. Bạn muốn dựng một Widget của riêng bạn cho Flutter thì cứ bắt tay extends một Flutter Widget mà thôi, tương đối dễ dàng hơn việc dựng một React Native Widget phức tạp, vốn có thể yêu cầu những thao tác cụ thể trên từng nền tảng đích để mỗi hành vi bạn gọi sẽ cho kết quả giống nhau trên cả Android và iOS. Và vì vậy, bạn sẽ tự định nghĩa hàm main này, vì main này là của Flutter chứ không phải của Java trong Android.

Quay trở lại với tên main() của main.dart, bo đì của nó sẽ định nghĩa tên tương-ứng-với-Activity-trên-Android, hay tương-ứng-với-ViewController-nào-trên-iOS sẽ chạy trước. Ngoài ra, có còn định nghĩa các routes. Đối với các bạn chưa học qua React Native sẽ mắt tròn mắt dẹt: “Ớ, đây là native app chứ có phải là trang web đâu mà routes?”. Tạm thời bạn cứ để đó, tới lúc thì tôi sẽ giải thích. Trong ví dụ mặc định “Xin chào thế giới” này, thì nó sẽ gọi MyApp ra. Èo, MyApp extends Widget à?

Nếu như bạn đã có “gốc” React Native, bạn có thể bỏ qua đoạn này chỉ với một câu duy nhất “Flutter widget tương đương React.Component”. Nếu chưa, mọi thứ đều là Widget, và bạn sẽ dùng một Widget chiếm trọn màn hình để giả lập luôn một Android Activity hay iOS ViewController, chứ không chỉ dừng ở mức contentView của chúng. Và tôi nhắc lại, việc chuyển cảnh “ảo” chỉ là việc chồng một Widget chiếm trọn màn hình khác lên, để nó chiếm lấy focus, chứ không phải thực sự chuyển sang một Activity hay ViewController mới, nó chỉ “mang lại cảm giác” mà thôi.

Mỗi Widget sẽ có một method có tên là build chứa một param duy nhất là BuildContext. Dù có tên làm bạn gợi nhớ tới Android Context nhưng chúng có thể nói là không có bà con gì cả, tức là “khác cha, khác ông nội và khác luôn các người thân khác”, chỉ là có tên na ná nhau, như ca sĩ Đan Trường và ông Dan Hauer, hay Harry Kane và Hari Won mà thôi. Giá trị mà nó return sẽ là nội dung bên trong Widget đó. Đối với một StatelessWidget hay State<StatefulWidget>, thì đây là một abstract method tương đương với onCreateView của Android Fragment.

Và bây giờ, mời bạn đọc lại trích đoạn code của main()MyApp:

Như được chỉ định, khi bạn bật ứng dụng Flutter này lên, thì tên main sẽ được gọi. Và nhiệm vụ của nó là chạy class MyApp lên. Và như tôi đã trình bày, tất cả đều là Widget. MyApp cũng là một Widget. Bạn tạm bỏ qua thắc mắc StatelessWidget là gì, hạ hồi sẽ phân giải sau. Còn bây giờ, hãy để ý phần return của MyApp#build: nó return một MaterialApp instance, và tên này cũng là một Widget luôn. Tên MaterialApp này có các thuộc tính được đề cập là title, theme, và home. Trong đó, tên title có ý nghĩa tương đương với Android @string/app_name để được đặt trong AndroidManifest với thuộc tính application:android:name, hay với iOS là Product Name, hay chí ít là Identity#Display name. Tên theme này có vai trò tương tự như phần theme trong values/styles của Android. Cuối cùng là home. Vai trò của tên này tương ứng với việc bạn định nghĩa Activity nào là MAIN/LAUNCHER với Android, hay ViewController nào sẽ được chạy đầu tiên trong AppDelegate.swift. Ngoài ra, nó còn có một prop khác chứa định nghĩa về các routes mà tôi sẽ nói sau.

Tên MaterialApp này sẽ đóng vai trò là Widget nằm dưới cùng, sẽ chứa các Widget để giả lập Android Activity hay iOS ViewController khác. Và nó có ảnh hưởng trực tiếp tới return value của _MyHomePageState#build, vốn chính là một instance của Scaffold. Tạm thời, bạn cứ chấp nhận máy móc những khái niệm như Scaffold, Column, Row, Center, Container, v.v… Những Widget đó sẽ được tôi hoặc giải thích đối với những Widget quan trọng cần được giải thích kĩ, hoặc gửi link chính chủ cho bạn tham khảo đối với những Widget kém quan trọng và nên được nghiên cứu sau.

Bây giờ, bạn sẽ nhìn lại prop home của MaterialApp bên trên, nó sẽ có value là một instance của MyHomePage như bên dưới. Và tạm thời, bạn chưa nên quan tâm tới những thứ liên quan tới State, mà chỉ nên nhìn nhận MyHomePage extends Widget:

Class StatefulWidget có một abstract method là createState, còn phần nội dung giao diện của nó phải nương nhờ tới createState.build, khá khác với StatelessWidget bên trên vốn có hàm build là abstract và bạn định nghĩa ngay trong đó. Vì sao lại như vậy? Tôi sẽ nhai tiếp câu “Hạ hồi phân giải” cho bạn một lần nữa. Còn bây giờ, trước khi tìm hiểu về _MyHomePageState, bạn cần nhìn lại constructor của MyHomePage, và một field member là String title, và createState sẽ có class là một State<SFW extends StatefulWidget> mà cụ thể là _MyHomePageState. Nhưng khoan đã, bạn có thấy String title trong MyHomePage giống một prop trong React và React Native không?

Scaffold đóng vai trò là một Layout cung cấp cho bạn những “ngăn chứa” để tạo giao diện theo kiểu Material Design trên Android. Chẳng hạn, appBar tương ứng với (AppBarLayout và) Toolbar, floatingActionButton tương ứng FloatingActionButton, drawer tương ứng NavigationDrawer, bottomNavigationBar tương ứng với BottomNavigationView, và body chính là phần giao diện chính. Trong khi đó, CenterColumn (và sau này là Row, Expanded, Flex, v.v…) là những Layout chứa các Widgets con bên trong. Thông thường, các Widgets con sẽ được đặt trong prop child, hay children với List<Widget> đối với các Layout có thể chứa nhiều Widgets con cùng tầng, chẳng hạn như Center chỉ chứa đúng một child duy nhất, trong khi Column lại chứa nhiều children.

Có lẽ bạn đang thắc mắc là vì sao lại có nhiều Layout tới vậy? Vì bạn không trực tiếp đặt vị trí của một Widget trên màn hình được, chí ít là thông qua margin hay padding. Bạn sẽ cần nhờ tới những Layout mang tính “chuyên dụng” cho một loại position duy nhất. Về Layout, tôi sẽ hướng dẫn các bạn trong bài sau.

Và bây giờ, để xem mặt mũi của ứng dụng mẫu này ra sao, bạn hãy cho chạy bằng nút Run màu xanh lá. Sang bài sau, chúng ta sẽ tiến hành tinh chỉnh project này, chẳng hạn như thay Column bằng Row, thêm Expanded, v.v… để bạn dễ nắm về các loại Layout này. Ngoài ra, Flutter hỗ trợ hot reload như RN, bạn chỉ cần bấm Ctrl S trên PC hay Command S trên Mac là giao diện Flutter trên máy ảo sẽ được cập nhật ngay mà không cần build lại ứng dụng.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.