Xin chào mọi người,
Chúng ta đã quá lâu không gặp lại nhau rồi đúng không?
Như đã nói từ kì trước, để tiếp tục series "Gitlab CI/CD cho người mới bắt đầu", thì ở bài này mình sẽ viết về cách set-up local gitlab runner, cụ thể hơn, chúng ta sẽ setting 1 runner qua Docker.
Tại sao lại là Docker chứ không phải là trên Window hay MacOS? Mình chọn Docker vì nó đơn giản với những người mới, cộng với tính linh động của nó trên nhiều môi trường khác nhau.
Vì vậy, nên để có thể tiếp tục với tutorial này thì việc đầu tiên bạn cần làm là có docker ở trên máy của mình.
(Bạn có thể xem hướng dẫn tải và cài đặt tại đây)
Nếu bạn đã cài đặt thành công Docker trên máy, thì chúng ta sẽ bắt đầu vào phần chính ngay thôi.
Về cơ bản thì việc khởi tạo và chạy runner qua Docker này sẽ trải qua 2 bước:
(Chi tiết có thể xem tại đây)
Trong các ngôn ngữ lập trình hướng đối tượng có một lỗi mà chúng ta rất hay gặp đó là NullPointerException, lỗi này có thể dẫn đến việc ứng dụng bị crash, ảnh hưởng đến trải nghiệm người dùng.
Để có thể giúp cho lập trình viên quản lý được việc nullable của một đối tượng từ đó từ khóa null safety xuất hiện.
Null safety cho phép bạn chỉ định việc một đối tượng có thể null hay không ngay từ khi khởi tạo. Và khi bạn muốn dùng nó compiler sẽ nhắc cho bạn để bạn có thể xử lý các exception liên quan đến việc biến có thể bị null
b. Null safety trong Flutter và Dart
Null safety bắt đầu có từ phiên bản Dart 2.12 và Flutter 2
Ở trạng thái mặc định những biến của bạn sẽ ở trạng thái non-nullable, điều đó có nghĩa là nó không thể null trừ khi bạn cho phép nó có thể. Với null safety các lỗi liên quan đến null sẽ chuyển từ các lỗi trong quá trình chạy thành các lỗi trong quá trình viết code. Điều này tránh những sai sót không đáng có trong quá trình chạy ứng dụng.
Trình biên dịch Dart có thể tối ưu hóa code, điều sẽ giúp cho project nhỏ hơn và chạy nhanh hơn.
Từ đó chúng ta có bài toán migrate những source code đã được viết trước phiên bản có null safety và biến nó thành source code có sử dụng null safety
2. Cách migrate từ source code không có null safety
a. Migrate code theo một thứ tự
Nếu chúng ta có sử dụng nhiều package, chúng ta nên lần lượt migrate chúng theo một thứ tự từ package được sử dụng nhiều nhất trong các package khác trước
b. Các bước migrate code sử dụng tool
Để demo cho phần này chúng ta sử dụng một class User
class User {
String name;
int age;
int yearOfBirth;
String des;
User({this.name, this.age});
set setCalculate(int cal){
yearOfBirth = cal;
}
}
Phiên bản của Dart
sdk: ‘>=2.10.0 <3.0.0'
Để bắt đầu chúng ta kiểm tra phiên bản của dart, hãy đảm bảo rằng bạn đang sử dụng phiên bản mới nhất của Dart SDK
dart --version
Tiếp theo là kiểm tra trạng thái của các dependency
dart pub outdated --mode=null-safety
Nếu output hiển thị ra như hình dưới đây thì bạn sẽ phải update các dependency
Sử dụng tool để migrate code
Trước khi bắt đầu migrate chúng ta cần biết về một vài hint marker – những thứ sẽ giúp chúng ta đánh dấu null safety cho các biến.
expression /*!*/
Thêm “!” vào trong code của bạn, biến biểu thức đó thành non-nullable
type /*!*/
Đánh dấu đối tượng đó là non-nullable
/*?*/
Đánh dấu đối tượng đó là nullable
/*late*/
Đánh dấu đối tượng đó được khởi tạo sau
/*late final*/
Đánh dấu đối tượng được khởi tạo sau nhưng chỉ được khởi tạo một lần
/*required*/
Đánh dấu tham số bị yêu cầu
Chúng ta sử dụng lệnh sau ở terminal để bắt đầu quá trình migrate:
dart migrate
Sau một thời gian sẽ có một đường link dẫn đến tool migration, giao diện như sau:
Đến đây chúng ta sẽ bắt đầu dùng những hint marker vào những chỗ được gợi ý dùng
Ở đây chúng ta mong muốn 3 biến name, age, yearOfBirth đều là non-nullable, vì vậy chúng ta thêm /*!*/ vào các biến đó, biến des có thể null vậy chúng ta thêm /*?*/.
Để tiến hành xem các thay đổi chúng ta ấn nút Rerun with changes, những thay đổi sẽ có trong code sau khi được migrate sẽ hiện ra
Giải thích:
Hai biến name và age được đánh dấu non-nullable, và được đặt trong cặp dấu ‘{}’ trong constructor, vì vậy từ khóa ‘required’ sẽ được thêm vào để yêu cầu giá trị init cho chúng khi đối tượng User mới được khởi tạo
Biến yearOfBirth được đánh dấu là non-nullable, tuy nhiên không xuất hiện trong constructor, vì vậy sẽ có thêm từ khóa ‘late’ từ khóa này
Biến des được đánh dấu là nullable do biến đó có thể null.
Sau khi kết thúc quá trình migration, chúng ta ấn nút ‘Apply migration’ để những thay đổi đó xuất hiện trên code của chúng ta
class User {
String name;
int age;
late int yearOfBirth;
String? des;
User({required this.name, required this.age});
set setCalculate(int cal){
yearOfBirth = cal;
}
}
Trên đây là code của lớp User sau khi được migrate
Cùng với sự thay đổi ở code, phiên bản của Dart cũng đã được update
sdk: ‘>=2.12.0 <3.0.0'
Đến đây là chúng ta đã hoàn thành quá trình migrate một đoạn code Dart sang null safety
Việc apply null safety đem lại rất nhiều lợi ích cho code của bạn, ngoài việc bắt kịp về mặt công nghệ nó còn giúp code của bạn an toàn hơn, chạy nhanh hơn.
Mục đích của blog này là cung cấp phân tích chi tiết về phân tích mô hình phát triển di động và các lựa chọn công nghệ, khuôn khổ khác nhau. Điều này sẽ giúp xác định con đường để chọn dựa trên các thông lệ kinh doanh và kỹ thuật.
CÁC MÔ HÌNH PHÁT TRIỂN ỨNG DỤNG
Cung cấp trải nghiệm di động đẳng cấp thế giới cho người dùng cuối liên quan đến các mô hình phát triển Ứng dụng khác nhau. Có 5 mô hình phát triển ứng dụng tồn tại:
Phát triển ứng dụng nền tảng(Native App Development)
Phát triển ứng dụng kết hợp (Hybrid App Development)
Phát triển ứng dụng nền tảng chéo (Cross Platform App Developmen)
Phát triển PWA (PWA Development)
Phát triển ứng dụng dành cho máy tính (Desktop App Development)
Native App Development Phát triển ứng dụng nền tảng liên quan đến việc tạo ra một ứng dụng di động chỉ phù hợp với một nền tảng. Trong trường hợp này, chúng ta sử dụng ngôn ngữ lập trình riêng cho hệ điều hành. Nó có thể là Java hoặc Kotlin cho Android và Swift / Objective-C cho iOS. Native Development có quyền truy cập vào bộ tính năng đầy đủ của thiết bị và do đó cho phép chúng tôi tận dụng chức năng nâng cao có hiệu suất cao. Công cụ sử dụng: XCode, ATOM, Android Studio, Android IDE. Ví dụ: Ghim sở thích, Cleartrip, v.v.
Hybrid App Development Các ứng dụng kết hợp kết hợp các yếu tố web với các yếu tố di động. Vì vậy, bạn tạo một cơ sở mã bằng cách sử dụng các công nghệ web tiêu chuẩn (HTML, CSS, JavaScript). Sau đó, bạn bọc nó bên trong một vùng chứa gốc – WebView. Các ứng dụng kết hợp chạy trong một trình duyệt toàn màn hình, được gọi là webview, ẩn đối với người dùng. Thông qua các plugin gốc có thể tùy chỉnh, họ có thể truy cập các tính năng gốc của thiết bị di động cụ thể (chẳng hạn như máy ảnh hoặc ID cảm ứng) mà không cần mã lõi bị ràng buộc với thiết bị đó. Công cụ sử dụng: Ionic, Cordova, v.v. Ví dụ: Evernote, Gmail, v.v.
Cross Platform App Development Phương pháp tiếp cận dành cho nhà phát triển đa nền tảng sử dụng công cụ kết xuất gốc. Cơ sở mã được viết bằng ngôn ngữ lập trình phổ biến (Phụ thuộc vào khuôn khổ) kết nối với các thành phần gốc thông qua cái gọi là cầu nối. Điều này cung cấp UX gần giống với nguyên bản. Ứng dụng đa nền tảng không có móc nối nền tảng. Chúng cung cấp chức năng liền mạch, dễ dàng triển khai và sản xuất hiệu quả về chi phí. Các công cụ sử dụng: React Native, NativeScripts, Flutter, v.v. Ví dụ: Uber Eats, GoogleAds, Alibaba, Instagram, v.v.
Progressive Web App Development PWAs cung cấp một cách tiếp cận thay thế cho phát triển ứng dụng di động truyền thống bằng cách bỏ qua việc phân phối cửa hàng ứng dụng và cài đặt ứng dụng. PWA là các ứng dụng web sử dụng một tập hợp các khả năng của trình duyệt – chẳng hạn như làm việc ngoại tuyến, chạy quy trình nền và thêm liên kết vào màn hình chính của thiết bị – để cung cấp trải nghiệm người dùng ‘giống ứng dụng’. Công cụ sử dụng: React, Angular JS, v.v. Ví dụ: Flipkart, Ali Express, Hầu hết được thấy trong nền tảng thương mại điện tử.
Desktop App Development
Phát triển ứng dụng dành cho máy tính đã tăng mức độ áp dụng trong những năm gần đây và có rất nhiều sự phát triển đang diễn ra để tạo ra ứng dụng máy tính đa nền tảng, như chuông, Slack, Spotify, v.v. Công cụ sử dụng: Electron cho ứng dụng máy tính đa nền tảng Ví dụ: Chime, Slack vv
CÁCH LỰA CHỌN MÔ HÌNH PHÁT TRIỂN – SO SÁNH
Trong phần này, chúng tôi sẽ phân tích một số lựa chọn khung và đánh giá chúng về kỹ thuật và phi kỹ thuật, đối với nghiên cứu này, chúng tôi đã chọn 3 mô hình phát triển:
Phát triển ứng dụng nền tảng(iOS và Android)
Ứng dụng kết hợp (Cordova)
Ứng dụng đa nền tảng (React Native, Flutter)
Phân tích kỹ thuật
Hiệu suất ứng dụng
Ứng dụng Hyrbid: Hiệu suất của Ứng dụng kết hợp bị tụt hậu so với nền tảng phát triển ứng dụng khác vì đó là chế độ xem web được bao bọc, Vỏ ứng dụng đang tương tác với thành phần gốc và do đó làm giảm hiệu suất ứng dụng tổng thể, đã nói rằng điều này cũng có thể dựa vào cách chúng tôi viết mã chế độ xem web, tất cả chức năng gốc mà chúng tôi đang xây dựng, v.v.
Ứng dụng nền tảng chéo: Nền tảng chéo cung cấp gần với hiệu suất ứng dụng gốc thực và đó là lý do tại sao hiệu suất tốt hơn so với kết hợp. Một trong những lý do là mã chúng ta viết trong khung chọn sẽ được biên dịch nguyên bản, có một chút khác biệt về hiệu suất tùy thuộc vào khung mà chúng ta chọn. 2.1. Flutter: Flutter sử dụng khung công tác Dart có hầu hết các thành phần có sẵn nên thường không yêu cầu cầu nối để giao tiếp với các mô-đun gốc, Dark Framework sử dụng Skia một công cụ c ++ và tất cả mã được biên dịch sẽ ở trong skia để có hiệu suất tốt hơn. 2.2. React Native: Kiến trúc React Native chủ yếu dựa vào kiến trúc môi trường thời gian chạy JS, còn được gọi là cầu nối JavaScript. Mã JavaScript được biên dịch thành mã gốc trong thời gian chạy. Tóm lại, React Native sử dụng cầu nối JavaScript để giao tiếp với các mô-đun gốc.
Ứng dụng Native: Bạn có thể không thể đánh bại các ngôn ngữ bản địa. Mã gốc được viết tốt sẽ luôn hoạt động hiệu quả hơn so với mã gốc đã được biên dịch hoặc mã kết hợp.
Quyền truy cập tính năng gốc
Ứng dụng kết hợp: Hầu hết các ứng dụng kết hợp đều sử dụng wireova hoặc tụ điện cung cấp các API JS cho các ứng dụng vốn thường được gọi là thành phần gốc cho bất kỳ quyền truy cập tính năng Gốc nào, có rất nhiều plugin của bên thứ 3 được yêu cầu cho bất kỳ ứng dụng kết hợp nào để sử dụng cho quyền truy cập tính năng gốc, hầu hết của plugin tính năng gốc sẽ có thể sử dụng được nhưng nếu được yêu cầu, nhà phát triển ứng dụng cũng có thể viết các trình bao bọc của riêng mình xung quanh các chức năng gốc và sau đó đưa chúng vào mã.
Ứng dụng nền tảng chéo: Nền tảng chéo cung cấp quyền truy cập tính năng gần giống như bản địa, rất khó sử dụng: 2.1. Flutter: Flutter cung cấp các gói chính thức cho hầu hết các tính năng gốc phổ biến và dễ dàng có sẵn trong hệ sinh thái rộng lớn. 2.2. React Native: React Native có tập hợp các gói bên thứ ba phong phú cũng như một số API có sẵn để truy cập tính năng gốc, dựa vào các plugin của bên thứ ba đi kèm với chi phí bảo trì.
Ứng dụng Native: Bạn có thể không thể đánh bại các ngôn ngữ native, truy cập trực tiếp vào thành phần bản địa và tất cả phần phát hành thành phần mới của hệ điều hành sẽ có thể sử dụng đầu tiên trong Phát triển ứng dụng bản địa.
Cơ sở mã đơn: Khả năng tái sử dụng
Ứng dụng Hyrbid: Ứng dụng kết hợp có khả năng phục hồi tuyệt vời vì đây là một chế độ xem web sử dụng HTML / CSS / JS (Tương tự như Web), nó đi kèm với cơ sở Mã đơn và mức độ khả năng phục hồi cao hơn.
Cross Platform App: Cross Platform cũng có độ phục hồi tốt nhưng không bằng hybrid, vẫn còn một số đoạn mã sẽ được viết riêng cho iOS / Android. 2.1. Flutter: Flutter widgets sử dụng thiết kế Material design trên cả hai nền tảng theo mặc định, có nghĩa là cùng một CX sẽ ở đó cho cả hai ứng dụng, nếu chúng ta cần thiết kế kiểu dáng khác nhau dựa trên nền tảng thì sẽ có một số công việc cần thiết dành riêng cho hệ điều hành. 2.2. React Native: React Native cung cấp một số thành phần giao diện người dùng cơ bản nhưng hầu hết chúng cần tự tạo kiểu, mã có thể được khôi phục vì chúng tôi đang sử dụng langugae lập trình đơn lẻ nhưng khoảng ~ 15-20% mã cần phải dành riêng cho nền tảng.
Ứng dụng Native: Rất khó để duy trì khả năng phục hồi trong phát triển ứng dụng gốc vì khung phát triển, ngôn ngữ khác nhau ở cả hai nền tảng.
Học một lần, viết ở mọi nơi
Ứng dụng Hyrbid: Ứng dụng kết hợp tất cả đều được viết trong khung phát triển web và học một công nghệ giúp tạo ứng dụng trên tất cả các nền tảng.
Ứng dụng đa nền tảng: Nền tảng chéo cũng sử dụng khái niệm tương tự, khung công tác khác nhau có các lựa chọn ngôn ngữ khác nhau nhưng ngôn ngữ duy nhất của nó được sử dụng để tạo ứng dụng trên tất cả các nền tảng. Ví dụ: Flutter sử dụng phi tiêu và React Native sử dụng JavaScript.
Ứng dụng Native: Tốt trong việc phát triển ứng dụng Gốc như một khung phát triển, ngôn ngữ khác nhau ở cả hai nền tảng, chúng ta cần tìm hiểu sâu về cả ngôn ngữ và nền tảng, ví dụ: iOS sử dụng XCode + Swift, Android sử dụng Kotling / Java.
Thành phần kiểu giao diện người dùng
Ứng dụng Hyrbid: Ứng dụng hỗn hợp có thành phần giao diện người dùng được tạo kiểu sẵn phụ thuộc vào khung giao diện người dùng bạn chọn, những cái nổi tiếng là ion. Những thành phần đó áp dụng cho cả hai nền tảng và mang lại trải nghiệm giống như bản địa về Kiểu giao diện người dùng.
Ứng dụng đa nền tảng: 2.1. Flutter: Flutter đã tích hợp sẵn các widget sử dụng thiết kế Material design và một số phong cách cupertino (cụ thể cho iOS) với tất cả các widget tích hợp này, bạn có thể tạo ứng dụng tuyệt vời mà chỉ cần tạo kiểu thủ công rất ít. 2.2. React Native: Các bản soạn phong cách giao diện người dùng phong phú có sẵn, rất nhiều plugin của bên thứ ba có thể sử dụng được yêu cầu nhà phát triển ứng dụng cũng phải viết kiểu riêng của họ.
Ứng dụng Native: Việc tuân thủ giao diện người dùng ứng dụng gốc là phổ biến và có thể sử dụng được cho tất cả các nền tảng gốc.
Thư viện bên thứ ba / Hệ sinh thái
Tất cả các nền tảng: Tất cả các nền tảng đều đã trưởng thành và có sẵn hệ sinh thái tốt nếu nhà phát triển gặp khó khăn.
Phân tích phi kỹ thuật
Mức độ phổ biến / Hỗ trợ cộng đồng
Ứng dụng Hyrbid: Ứng dụng kết hợp có Mức độ phổ biến đã giảm dần trong một số năm khi các nhà phát triển trong gần như cùng thời gian có thể tạo ứng dụng giống như bản địa bằng cách chọn nền tảng phát triển ứng dụng khác.
Ứng dụng nền tảng chéo: Nền tảng chéo đã cho thấy sự phổ biến to lớn sau khi ra mắt công khai phản ứng gốc vào năm 2015 2.1. Flutter: Flutter được google ra mắt công khai vào khoảng năm 2017 và đã đạt được sự phát triển vượt bậc và rất được nhiều công ty lớn chấp nhận để tạo ứng dụng đa nền tảng. 2.2. React Native: React Native dẫn đầu về phát triển ứng dụng, rất nhiều hỗ trợ từ cộng đồng, stackoverflow, github repositry (80k Stars trong github)
Ứng dụng Native: Mặc dù việc phát triển ứng dụng khác đã phổ biến nhưng sự phát triển ứng dụng gốc vẫn đang bùng nổ ngày nay vì trải nghiệm và hiệu suất ứng dụng liền mạch. Rất nhiều công ty lớn đã thử sử dụng nền tảng kết hợp / chéo và cuối cùng đã tạo ra ứng dụng ở dạng bản địa. Ứng dụng ban đầu Ex Cleartrip là ứng dụng lai và sau đó được viết lại hoàn toàn bằng bản địa.
Thời gian đưa ra thị trường
Ứng dụng Hyrbid: Thời gian đưa ra thị trường sẽ ít hơn đối với việc phát triển Ứng dụng lai vì hệ số xem web và khả năng tái sử dụng của nó rất cao cũng như cơ sở mã duy nhất giúp thời gian đưa ra thị trường nhanh hơn.
Ứng dụng Cross Platform: Thời gian tiếp thị trên nhiều nền tảng vẫn ít hơn so với ứng dụng gốc, lý do để đặt nó không ngang bằng với ứng dụng kết hợp là vẫn còn đường cong học tập ban đầu có thể làm tăng thời gian tiếp thị cho các ứng dụng này.
Ứng dụng Native: Đường cong học tập, duy trì hai ngăn xếp khác nhau, cơ sở mã, v.v. khiến thời gian đưa ra thị trường chậm hơn tất cả các tùy chọn khác.
Chi phí phát triển
Ứng dụng Hyrbid: Chi phí phát triển sẽ thấp hơn nhiều vì cùng một nhóm triển khai mã ứng dụng web có thể được sử dụng để tạo các trang web trong ứng dụng kết hợp.
Ứng dụng Cross Platform: Chi phí đa nền tảng sẽ cao hơn một chút, vì sẽ có một số đường học tập về công nghệ mới và cũng yêu cầu một số kiến thức phát triển ứng dụng gốc.
Ứng dụng Native: Chi phí phát triển cao hơn vì bảo trì / phát triển hai cơ sở mã yêu cầu kiến thức chuyên môn đặc biệt.
Thuê mướn
Ứng dụng Hyrbid: Không cần thuê bất kỳ người mới nào vì cùng một nhà phát triển đang phát triển ứng dụng web có thể viết ứng dụng kết hợp.
Ứng dụng đa nền tảng: có thể yêu cầu hoặc không vì nó yêu cầu phát triển khung cụ thể, chúng tôi đã thấy việc thuê tổ chức chỉ phản ứng nhà phát triển bản địa hoặc nhà phát triển chập chờn.
Ứng dụng Native: Cần thuê hoặc duy trì hai nhóm dành riêng cho Android và iOS.
Chi phí bảo trì
Thời gian phát triển
Trưởng thành
Ứng dụng Hyrbid: Ứng dụng kết hợp vẫn là ứng dụng đầu tiên ra đời sau khi bản địa vẫn chưa hoàn thiện về các tính năng truy cập bản địa.
Ứng dụng nền tảng chéo: Nền tảng chéo đã cho thấy sự tuyệt vời và rất nhiều ứng dụng đa nền tảng như uber eat, instagram, quảng cáo goodle, alibaba được tạo ra ở đó ứng dụng trên ứng dụng đa nền tảng. 2.1. Flutter: Flutter được google ra mắt công chúng vào khoảng năm 2017 và đã đạt được sự phát triển vượt bậc và nó rất được nhiều công ty lớn chấp nhận để tạo ra ứng dụng đa nền tảng, vì nó vẫn còn ở giai đoạn đầu, tôi chỉ xếp nó sau RN, điều này chắc chắn sẽ xảy ra trong thời gian tới. 2.2. React Native: React Native dẫn đầu về phát triển ứng dụng, rất nhiều hỗ trợ từ cộng đồng, stackoverflow, github repositry (80k Stars trong github).
Ứng dụng Native: Mặc dù việc phát triển ứng dụng khác đã phổ biến nhưng sự phát triển ứng dụng gốc vẫn đang bùng nổ ngày nay vì trải nghiệm và hiệu suất ứng dụng liền mạch. Các công ty phụ thuộc rất nhiều vào trải nghiệm di động chủ yếu chọn phát triển ứng dụng gốc như một cách thuần thục để tạo ứng dụng cho người dùng
Theo nghĩa rộng nhất có thể, trạng thái của ứng dụng là mọi thứ tồn tại trong bộ nhớ khi ứng dụng đang chạy. Điều này bao gồm nội dung của ứng dụng, tất cả các biến mà khung Flutter lưu giữ về giao diện người dùng, trạng thái hoạt ảnh, kết cấu, phông chữ, v.v. Mặc dù định nghĩa rộng nhất có thể này về trạng thái là hợp lệ, nhưng nó không hữu ích lắm cho việc kiến trúc một ứng dụng.
Đầu tiên, bạn thậm chí không quản lý một số trạng thái (như kết cấu). Khung xử lý những điều đó cho bạn. Vì vậy, một định nghĩa hữu ích hơn về trạng thái là “bất kỳ dữ liệu nào bạn cần để xây dựng lại giao diện người dùng của bạn bất kỳ lúc nào”. Thứ hai, trạng thái mà bạn tự quản lý có thể được tách thành hai loại khái niệm: ephemeral state and app state.
Ephemeral state
Ephemeral state (đôi khi được gọi là UI state or local state) là trạng thái bạn có thể chứa gọn gàng trong một tiện ích con.
Đây là một định nghĩa mơ hồ, vì vậy đây là một vài ví dụ:
Page hiện tại trong PageView
Tiến hành hiện tại trong một animation phức tạp.
Tab được chọn hiện tại trong BottomNavigationBar.
Các phần khác của widget tree hiếm khi cần phải truy cập vào loại trạng thái này. Không cần phải tuần tự hóa nó và nó không thay đổi theo những cách phức tạp.
Nói cách khác, không cần sử dụng các kỹ thuật quản lý trạng thái (ScopedModel, Redux, v.v.) trên loại trạng thái này. Tất cả những gì bạn cần là một StatefulWidget.
Dưới đây, bạn thấy cách mục hiện được chọn trong thanh điều hướng dưới cùng được giữ trong trường _index của lớp _MyHomepageState. Trong ví dụ này, _index là ephemeral state.
Ở đây, việc sử dụng setState() và một trường bên trong lớp StatefulWidget’s State là hoàn toàn tự nhiên. Không phần nào khác trong ứng dụng của bạn cần truy cập _index. Biến chỉ thay đổi bên trong tiện ích MyHomepage. Và, nếu người dùng đóng và khởi động lại ứng dụng, bạn đừng bận tâm rằng _index đặt lại về 0.
App state
Đây không phải là ephemeral state mà bạn muốn chia sẻ trên nhiều phần của ứng dụng và bạn muốn giữ lại giữa các phiên của người dùng, chúng ta gọi là application state (đôi khi còn được gọi shared state).
Ví dụ về application state:
Sở thích của người sử dụng
Thông tin đăng nhập
Thông báo trong một ứng dụng mạng xã hội
Giỏ hàng trong ứng dụng thương mại điện tử
Trạng thái đã đọc / chưa đọc của các bài báo trong một ứng dụng tin tức
Để quản lý app state, bạn sẽ muốn nghiên cứu các tùy chọn của mình. Lựa chọn của bạn phụ thuộc vào mức độ phức tạp và bản chất của ứng dụng, trải nghiệm trước đây của nhóm bạn và nhiều khía cạnh khác.
Không có quy tắc rõ ràng.
Để rõ ràng hơn, bạn có thể sử dụng State và setState() để quản lý tất cả các trạng thái trong ứng dụng của mình. Trên thực tế, nhóm Flutter thực hiện điều này trong nhiều mẫu ứng dụng đơn giản (bao gồm cả ứng dụng dành cho người mới bắt đầu mà bạn nhận được sau mỗi lần tạo Flutter).
Nó cũng đi theo hướng khác. Ví dụ: bạn có thể quyết định rằng — trong ngữ cảnh của ứng dụng cụ thể của bạn — tab đã chọn trong thanh điều hướng dưới cùng không phải là trạng thái tạm thời. Bạn có thể cần phải thay đổi nó từ bên ngoài lớp học, giữ nó giữa các phiên, v.v. Trong trường hợp đó, biến _index là trạng thái ứng dụng.
Không có quy tắc chung, rõ ràng nào để phân biệt một biến cụ thể là ephemeral sate hay trạng thái ứng dụng. Đôi khi, bạn sẽ phải cấu trúc lại cái này thành cái khác. Ví dụ: bạn sẽ bắt đầu với một số trạng thái rõ ràng là tạm thời, nhưng khi ứng dụng của bạn phát triển về các tính năng, nó có thể cần được chuyển sang app state.
Vì lý do đó, hãy lấy sơ đồ sau với một lượng muối lớn:
Để hiểu điều này trước tiên chúng ta hãy hiểu Stateful Builder là gì?
StatefulBuilder là một widget có trạng thái có thể thay đổi (trạng thái có thể thay đổi), điều làm cho nó trở nên đặc biệt là chỉ xây dựng lại widget cụ thể được bao bọc trong Stateful Builder. Rất may là không có việc xây dựng lại toàn bộ widget, không có vấn đề gì widget được bao bọc trong Stateful Builder nằm trong Stateless hay stateful Widget.
Bây giờ câu hỏi đặt ra là chúng ta biết rằng nó không thể xây dựng lại Stateless Widget cho đến khi có một phiên bản mới. (SetState không hoạt động)
Stateful builder có sức mạnh của StateSetter chức năng được truyền cho trình xây dựng được sử dụng để gọi một quá trình xây dựng lại chính nó thay vì một trạng thái điển hình của toàn bộ Widget.
Bất kỳ biến nào đại diện cho trạng thái nên được giữ bên ngoài hàm build có nghĩa là Bạn phải khai báo biến bên ngoài phương thức build để thay đổi xảy ra.
Như tôi đã đề cập ở trên, nó chỉ xây dựng lại một widget cụ thể. Nó chỉ cung cấp một thể hiện mới (State) cho widget cụ thể đó. vì vậy bạn đã gói tất cả widget (chế độ xem) mà bạn muốn thấy sự thay đổi trong StatefulBuilder.
Để hiểu Tại sao Stateful Builder không thể thay thế một Stateful widget. Đầu tiên chúng ta hãy hiểu điều gì tạo nên sự khác biệt giữa Widget không trạng thái và Widget trạng thái.
Bạn có nhận thấy trong phương pháp xây dựng ghi đè không trạng thái và mặt khác trong tiện ích con Stateful trước tiên sẽ ghi đè createState và sau đó ghi đè phương thức build? Điều xảy ra ngầm là khi widget không trạng thái nhận được một phiên bản mà hàm xây dựng của nó được gọi. và trong widget trạng thái trước tiên, hãy tạo thể hiện trạng thái để xây dựng hàm.Đó là cách setState đang hoạt động. Sau khi chạy setState, createState đưa ra một thể hiện mới để xây dựng lại phương thức và tiện ích con.
Giờ chúng ta sẽ hiểu tại sao Stateful Builder không thể thay thế tiện ích Stateful.
Nếu nó đang xây dựng lại phần cụ thể của cây widget. Tại sao chúng ta không thể sử dụng thay thế cho Stateful Widget?
Nếu bạn lưu trữ bất kỳ trạng thái nào trong các biến cá thể trong widget không trạng thái, nó có thể bị mất bất cứ lúc nào vì hàm tạo cho widget không trạng thái có thể được gọi lặp lại, giống như phương thức build. Chỉ có Trạng thái phiên bản liên quan của tiện ích con Stateful được lưu giữ bởi Flutter.
StatefulWidget vẫn được giữ nguyên trạng thái của nó ngay cả trong các lần xây dựng lại, đó là lý do tại sao đôi khi bạn thấy các hoạt động sai của TextEditorController trong Stateless Widget
Hoặc nếu bạn không đủ khả năng sử dụng StatefulBuilder với Stateless Widget thì bạn phải lưu trạng thái bằng các khóa Toàn cục.
Cảm ơn bạn đã đọc bài viết này nếu bạn thấy bất cứ điều gì có thể được cải thiện, vui lòng cho tôi biết, tôi rất muốn cải thiện.
Automatic Reference Counting aka ARC, là 1 tính năng của Swift dùng để đếm số lượng strong reference, và cho phép quản lý việc giải phóng memory khi 1 instance không còn được reference(tham chiếu) đến.
Theo như doc của Apple:
Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you don’t need to think about memory management yourself. ARC automatically frees up the memory used by class instances when those instances are no longer needed.
Chúng ta cũng cần nhớ rằng trong swift một reference của được định nghĩa mặc định là kiểu strong
Ví dụ :
class FirstClass: UIViewController {
let secondClass = SecondClass()
}
class SecondClass {
}
Trong ví dụ này, class FirstViewController của mình đang strong reference đến instance secondClass
2-Vấn đề của strong reference cycle
Như mình đã đề cập bên trên, ARC sẽ giúp chúng ta quản lý, phân bổ memory từ những instance không còn được tham chiếu đến.
Tuy nhiên, câu hỏi đặt ra là điều gì sẽ xảy ra khi có 2 object/instance strong reference đến nhau?
Từ strong việt hóa ra là mạnh, bền, … nghe thôi cũng cảm giác khó phá hỏng, phá hủy nó rồi đúng không? :)))
Thì strong reference cũng thế, strong reference trỏ đến 1 instance/object và sở hữu nó, quyết định đến sự tồn tại của instance/object đó.
ARC không thể có cách nào giải phóng được memory từ những kiểu instace/object này, và điều này sẽ dẫn đến memory leak.
Trong thực tế những project đã làm, thì mình rất hay thường gặp những case strong reference, và mình cũng rất hay vô tình tạo ra strong reference :v
Case mình hay gặp nhất là case khi khởi tạo delegate:
protocol DemoDelegate {
func demoFunc()
}
class FirstClass {
var delegate: DemoDelegate?
}
Trông ví dụ này có vẻ quen đúng không? :))) ở đây mình có 1 protocol DemoDelegate, và khởi tạo delegate này trong FirstClass. Và FirstClass và delegate DemoDelegate đang strong reference đến nhau.
Để giải quyết vấn đề này, chúng ta có weak và unowned.
Weak và Unowned
Weak
Trái ngược với strong pointer, weak pointer trỏ đến một instance/object và không quyết định đến sự tồn tại của instance/object đó.
Vì thế nên ARC có thể dễ dàng giải phóng bộ nhớ từ instance/object này kể cả chúng ta vẫn còn đang tham chiếu đến nó
=> Weak reference rất hữu ich, giúp chúng ra tránh được việc vô tình hay cố ý tạo ra 1 strong reference cycle.
Note: Weak reference không thể sử dụng với việc khởi tạo instance/object là let bởi vì instance/object ở một vài case vẫn phải truyền nil.
Việc sử dụng weak reference thực sự quan trọng, ví dụ nếu chúng ta để ý hơn và xem source code bên trong definition của UITableView, ta có thể nhận thấy rằng tất cả delegate(bao gồm 2 var quen thuộc là dataSource và delegate) đều được sử dụng với weak khi khởi tạo.
Và mình nghĩ là đến apple còn để ý và chú trọng đến việc sử dụng weak, tại sao chúng ta lại không làm như thế ? :v
protocol DemoDelegate {
func demoFunc()
}
class FirstClass {
weak var delegate: DemoDelegate?
}
Ok, thêm weak là xong, đơn giản phải không nào.
Tuy nhiên, khi thêm weak, lại nảy sinh vấn đề không thể compile đoạn code:
Để giải quyết, theo doc Protocols của apple:
Use a class-only protocol when the behavior defined by that protocol’s requirements assumes or requires that a conforming type has reference semantics rather than value semantics.
Để fix, chỉ cần thêm keyword class là được
protocol DemoDelegate: class {
func demoFunc()
}
class FirstClass {
weak var delegate: DemoDelegate?
}
2. unowned
Một variable kiểu unownevề cơ bản giống với variable kiểu weak, nhưng khác nhau ở chỗ: compiler sẽ make sure rằng variable này khi được gọi đến sẽ không có giá trị nil.
Vậy thực sự trong trường hợp nào nên sử dụng unowned thay vì sử dụng weak ? Vẫn theo doc của Apple:
Like a weak reference, an unowned reference doesn’t keep a strong hold on the instance it refers to. Unlike a weak reference, however, an unowned reference is used when the other instance has the same lifetime or a longer lifetime.
Để hiểu rõ hơn, mình có 2 ví dụ như này
Ví dụ của weak reference: mình sở hữu 1 chiếc xe đạp, nhưng 1 hôm đẹp trời nào đó, mình đổi ý không muốn đạp xe nữa, thì ở đây chiếc xe đạp vẫn còn đó, có chăng chỉ là đổi chủ, trong khi mình đang ngồi 1 chiếc 4 bánh nào đó :v
Ví dụ của unowned reference: mình chơi 1 yasuo chẳng hạn :))), thằng nhân vật của mình tăng skill theo cấp độ. Tuy nhiên, khi xám màn hình, những skill đó cũng sẽ xám theo. Có nghĩa rằng là những skill này có tuổi thọ = tuổi thọ của nhân vật mình đang chơi.
Triển khai trong code:
class Yasuo {
let name: String
let skill: Skill?
init(name: String) {
self.name = name
}
}
class Skill {
let damage: Int
unowned let champ: Yasuo
init(damage: Int, champ: Yasuo) {
self.damage= power
self.champ = champ
}
}
3-Debugging
Vậy là chúng ta đã hiểu phần nào về 2 keyword weak và unowned. Chúng dùng để tránh tình trạng vô tình tạo ra memory leak.
Nhưng sao để biết được có thực sự tránh được hay không?
Cách đơn giản và xưa như quả đất, là sử dụng print trong 2 method init và deinit
class FirstClass {
init() {
print("FirstClass is being initialized")
}
deinit {
print("FirstClass is being deinitialized")
}
}
Và để ý memory trong XCode nữa :v
4-Tổng kết
Strong, weak, unowned là những thuật ngữ cơ bản trong swift, tuy nhiên chúng ta thường – do vô ý hoặc chủ quan – bỏ qua chúng.
Điều này không thực sự ảnh hưởng quá lớn với những project nhỏ.
Tuy nhiên với những app lớn, việc quản lý bộ nhớ ra sao cho hiệu quả là 1 việc quan trọng và đáng lưu ý, bởi vì khi memory leak trở thành 1 vấn đề nghiêm trọng, thì rất khó và tốn effort để fix. 😀
Ứng dụng có hai màn hình riêng biệt: danh mục và giỏ hàng (được đại diện bởi các tiện ích MyCatalog và MyCart, tương ứng). Nó có thể là một ứng dụng mua sắm, nhưng bạn có thể tưởng tượng cấu trúc tương tự trong một ứng dụng mạng xã hội đơn giản (thay thế danh mục cho “tường” và giỏ hàng cho “yêu thích”).
Màn hình danh mục bao gồm thanh ứng dụng tùy chỉnh (MyAppBar) và dạng xem cuộn của nhiều mục danh sách (MyListItems).
Đây là ứng dụng được hình dung dưới dạng cây tiện ích con.
Vì vậy, chúng ta có ít nhất 5 lớp con của Widget. Nhiều người trong số họ cần quyền truy cập vào trạng thái “thuộc về” ở nơi khác. Ví dụ: mỗi MyListItem cần có khả năng tự thêm vào giỏ hàng. Nó cũng có thể muốn xem liệu mặt hàng được hiển thị hiện đã có trong giỏ hàng hay chưa.
Điều này đưa chúng ta đến câu hỏi đầu tiên: chúng ta nên đặt trạng thái hiện tại của giỏ hàng ở đâu?
Đưa trạng thái lên
Trong Flutter, bạn nên giữ trạng thái ở trên các widget sử dụng nó.
Tại sao? Trong các khung công tác khai báo như Flutter, nếu bạn muốn thay đổi giao diện người dùng, bạn phải xây dựng lại nó. Không có cách nào dễ dàng để có MyCart.updateWith (somethingNew). Nói cách khác, thật khó để thay đổi thứ bậc một tiện ích từ bên ngoài, bằng cách gọi một phương thức trên đó. Và ngay cả khi bạn có thể làm cho điều này thành công, bạn sẽ chiến đấu với khuôn khổ thay vì để nó giúp bạn.
Ngay cả khi code trên hoạt động, bạn sẽ phải xử lý những điều sau trong tiện ích MyCart:
Bạn sẽ cần phải xem xét trạng thái hiện tại của giao diện người dùng và áp dụng dữ liệu mới cho nó. Thật khó để tránh lỗi theo cách này.
Trong Flutter, bạn tạo một widget mới mỗi khi nội dung của nó thay đổi. Thay vì MyCart.updateWith (somethingNew) (một cuộc gọi phương thức), bạn sử dụng MyCart (nội dung) (một phương thức khởi tạo). Vì bạn chỉ có thể tạo các tiện ích con mới trong các phương pháp tạo của cha mẹ chúng, nên nếu bạn muốn thay đổi nội dung, tiện ích con đó phải ở chế độ gốc của MyCart trở lên.
Bây giờ MyCart chỉ có một đường dẫn mã để xây dựng bất kỳ phiên bản giao diện người dùng nào.
Trong ví dụ của chúng ta, nội dung cần phải tồn tại trong MyApp. Bất cứ khi nào nó thay đổi, nó sẽ xây dựng lại MyCart từ bên trên (sẽ nói thêm về điều đó sau). Do đó, MyCart không cần phải lo lắng về vòng đời — nó chỉ khai báo những gì sẽ hiển thị cho bất kỳ nội dung nhất định nào. Khi điều đó thay đổi, tiện ích MyCart cũ sẽ biến mất và được thay thế hoàn toàn bằng tiện ích mới.
Đây là ý của chúng ta khi nói rằng widget là bất biến. Chúng không thay đổi — chúng được thay thế.
Bây giờ chúng ta đã biết nơi đặt trạng thái của giỏ hàng, hãy xem cách truy cập vào nó.
Truy cập trạng thái
Khi người dùng nhấp vào một trong các mặt hàng trong danh mục, mặt hàng đó sẽ được thêm vào giỏ hàng. Nhưng kể từ khi giỏ hàng nằm trên MyListItem, chúng ta làm điều đó như thế nào?
Một tùy chọn đơn giản là cung cấp một lệnh gọi lại mà MyListItem có thể gọi khi nó được nhấp vào. Các hàm của Dart là các đối tượng hạng nhất, vì vậy bạn có thể chuyển chúng theo bất kỳ cách nào bạn muốn. Vì vậy, bên trong MyCatalog, bạn có thể xác định những điều sau:
Điều này hoạt động tốt, nhưng đối với một trạng thái ứng dụng mà bạn cần sửa đổi từ nhiều nơi khác nhau, bạn sẽ phải chuyển qua nhiều lần gọi lại — điều này sẽ cũ đi khá nhanh.
May mắn thay, Flutter có các cơ chế để các widget cung cấp dữ liệu và dịch vụ cho con cháu của chúng (nói cách khác, không chỉ con cái của chúng mà bất kỳ widget nào bên dưới chúng). Như bạn mong đợi từ Flutter, nơi mọi thứ đều là Widget, các cơ chế này chỉ là những loại widget đặc biệt — InheritedWidget, InheritedNotifier, InheritedModel, v.v. Chúng ta sẽ không đề cập đến những điều đó ở đây, vì chúng hơi thấp so với những gì chúng ta đang cố gắng thực hiện.
Thay vào đó, chúng ta sẽ sử dụng một gói hoạt động với các widget cấp thấp nhưng dễ sử dụng. Nó được gọi là nhà cung cấp.
Trước khi làm việc với nhà cung cấp, đừng quên thêm phần phụ thuộc vào nhà cung cấp đó vào pubspec.yaml của bạn.
Bây giờ bạn có import ‘package:provider/provider.dart’; và bắt đầu xây dựng.
Với provider, bạn không cần phải lo lắng về các cuộc gọi lại hoặc InheritedWidgets. Nhưng bạn cần hiểu 3 khái niệm:
ChangeNotifier
ChangeNotifierProvider
Consumer
ChangeNotifier
ChangeNotifier là một lớp đơn giản có trong Flutter SDK cung cấp thông báo thay đổi cho người nghe của nó. Nói cách khác, nếu thứ gì đó là ChangeNotifier, bạn có thể đăng ký các thay đổi của nó. (Đây là một dạng có thể quan sát được, dành cho những người quen thuộc với thuật ngữ này.)
Trong provider, ChangeNotifier là một cách để đóng gói trạng thái ứng dụng của bạn. Đối với các ứng dụng rất đơn giản, bạn có thể sử dụng với một ChangeNotifier duy nhất. Trong những cái phức tạp, bạn sẽ có một số mô hình và do đó là một số ChangeNotifier. (Bạn hoàn toàn không cần sử dụng ChangeNotifier với provider, nhưng đó là một lớp dễ làm việc.)
Trong ví dụ về ứng dụng mua sắm của chúng ta, chúng ta muốn quản lý trạng thái của giỏ hàng trong ChangeNotifier. Chúng ta tạo một lớp mới để mở rộng nó, giống như vậy
Code duy nhất dành riêng cho ChangeNotifier là lệnh gọi tới notificationListists (). Gọi phương thức này bất cứ khi nào mô hình thay đổi theo cách có thể thay đổi giao diện người dùng của ứng dụng của bạn. Mọi thứ khác trong CartModel là chính mô hình và logic của nó.
ChangeNotifierProvider
ChangeNotifierProvider là widget cung cấp một phiên bản của ChangeNotifier cho con cháu của nó. Nó đến từ provider package.
Chúng ta đã biết nơi đặt ChangeNotifierProvider: phía trên các widget cần truy cập nó. Trong trường hợp của CartModel, điều đó có nghĩa là ở đâu đó trên cả MyCart và MyCatalog.
Bạn không muốn đặt ChangeNotifierProvider cao hơn mức cần thiết (vì bạn không muốn làm giảm hiệu suất). Nhưng trong trường hợp của chúng ta, tiện ích duy nhất nằm trên cả MyCart và MyCatalog là MyApp.
Consumer
Bây giờ CartModel đã được cung cấp cho các widget trong ứng dụng của chúng ta thông qua khai báo ChangeNotifierProvider ở trên cùng, chúng ta có thể bắt đầu sử dụng nó.
Điều này được thực hiện thông qua tiện ích Consumer widget.
Chúng ta phải chỉ định loại mô hình mà chúng ta muốn truy cập. Trong trường hợp này, chúng ta muốn CartModel, vì vậy chúng ta viết Consumer <CartModel>. Nếu bạn không chỉ định chung chung (<CartModel>), provider sẽ không thể giúp bạn. Provider dựa trên các loại và nếu không có loại, nó không biết bạn muốn gì.
Đối số bắt buộc duy nhất của Consumer Widget là trình tạo. Builder là một hàm được gọi bất cứ khi nào ChangeNotifier thay đổi. (Nói cách khác, khi bạn gọi thông báo () trong mô hình của mình, tất cả các phương thức trình tạo của tất cả Consumer Widget tương ứng sẽ được gọi.)
Trình tạo được gọi với ba đối số. Điều đầu tiên là ngữ cảnh, mà bạn cũng nhận được trong mọi phương pháp xây dựng.
Đối số thứ hai của hàm trình tạo là phiên bản của ChangeNotifier. Đó là những gì chúng ta đã yêu cầu ngay từ đầu. Bạn có thể sử dụng dữ liệu trong mô hình để xác định giao diện người dùng sẽ trông như thế nào tại bất kỳ điểm nhất định nào.
Đối số thứ ba là con, ở đó để tối ưu hóa. Nếu bạn có một cây con tiện ích con lớn trong Consumer Widget không thay đổi khi mô hình thay đổi, bạn có thể tạo nó một lần và lấy nó thông qua trình tạo.
Sự phát triển của Flutter đang trở nên phổ biến và ngày càng phổ biến hơn do có nhiều tùy biến, widget tùy chỉnh và cách tiếp cận rất dễ thực hiện. Hôm nay chúng ta sẽ tìm hiểu cách tạo nút radio tùy chỉnh Flutter với các hình dạng tùy chỉnh như hộp đựng hình vuông, hộp đựng hình tròn hoặc biểu tượng. Tôi đã cố gắng tạo ra các phương pháp khác nhau để chúng có thể được sử dụng theo nhu cầu và nhà phát triển sẽ học từ hướng dẫn này cũng có thể có ý tưởng tạo nút radio tùy chỉnh rung của riêng mình, với hình dạng hoặc tiện ích theo yêu cầu của mình.
Ví dụ về Nút radio tùy chỉnh Flutter này sẽ tăng tốc độ phát triển của bạn và tiết kiệm thời gian của bạn bằng cách gọi nó trực tiếp như một phần tử con của bất kỳ tiện ích nào hoặc bằng cách thực hiện các phương thức tĩnh khi nó cần thiết. Bởi vì không có ứng dụng nào có thể được thực hiện mà không thực hiện bất kỳ widegt tùy chỉnh nào, tất cả các widget được tạo sẵn không phải lúc nào cũng giúp ích được. Vì vậy, học cách chế tạo widget tùy chỉnh là phần quan trọng nhất của quá trình phát triển Flagship.
Hãy bắt đầu thực hiện từng bước để chúng ta có thể hiểu những gì cần phải làm để đạt được kết quả đầu ra mong muốn. Đầu tiên, chúng ta sẽ tạo widget home, nơi chúng ta sẽ hiển thị các nút radio tùy chỉnh.
Nó là một widget toàn diện có khả năng xử lý tất cả các chức năng của nút radio, như hiển thị thiết kế tùy chỉnh, sự kiện chạm và lựa chọn, v.v.
Lớp này chịu trách nhiệm xử lý tất cả các hoạt động của nút radio tùy chỉnh, như thiết kế, sự kiện và gọi lại. Nếu bạn nhận thấy, tôi đã thêm một chức năng gọi lại để trả về chỉ mục mục được nhấn hiện tại. Trong trường hợp bạn muốn làm cho lớp nút radio tùy chỉnh của mình độc lập và chung chung, bạn có thể tạo cho các sự kiện theo yêu cầu của mình.
final Function(int)? onItemSelected;
my_radio_button_widget.dart
class MyRadioButtonWidget extends StatefulWidget {
final List<RadioButtonModel>? options;
final Function(int)? onItemSelected;
const MyRadioButtonWidget({Key? key, this.options, this.onItemSelected})
: super(key: key);
@override
createState() {
return MyRadioButtonWidgetState();
}
}
class MyRadioButtonWidgetState extends State<MyRadioButtonWidget> {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: widget.options!.length,
itemBuilder: (BuildContext context, int index) {
return InkWell(
onTap: () {
setState(() {
for (var element in widget.options!) {
element.isSelected = false;
}
widget.options![index].isSelected = true;
widget.onItemSelected!(index);
});
},
child: CircleRadioButtonItem(widget.options![index]),
);
},
);
}
}
Radio Button Model
class RadioButtonModel {
bool isSelected;
final String buttonText;
final String text;
RadioButtonModel(this.isSelected, this.buttonText, this.text);
}
Bây giờ chúng tôi sẽ tạo các hình dạng tùy chỉnh cho nút radio của chúng ta, ta có nhiều tùy chọn, bạn có thể tìm thấy ba lớp khác nhau để thể hiện phong cách khác nhau. bằng cách sử dụng ý tưởng này, bạn có thể tự tạo và sửa đổi các ví dụ đã cho theo ý muốn.
Nút radio vuông
Ở đây chúng tôi đang tạo hộp chứa hình vuông với văn bản bên trong nó.
Trong một hệ thống máy tính hiện đại, thường có một số tiến trình ứng dụng đồng thời muốn thực thi. Giờ đây, hệ điều hành có trách nhiệm quản lý tất cả các quy trình một cách hiệu quả và hiệu quả.
Một trong những khía cạnh quan trọng nhất của hệ điều hành là đa chương trình.
Trong một hệ thống máy tính, có nhiều quá trình đang chờ được thực thi, tức là chúng đang đợi khi CPU sẽ được cấp phát cho chúng và chúng bắt đầu thực thi. Các quy trình này còn được gọi là công việc. Bây giờ bộ nhớ chính quá nhỏ để chứa tất cả các quy trình hoặc công việc này vào đó. Do đó, các quy trình này ban đầu được lưu giữ trong một khu vực được gọi là nhóm công việc. Nhóm công việc này bao gồm tất cả các quy trình đang chờ cấp phát bộ nhớ chính và CPU.
CPU chọn một công việc trong số tất cả các công việc đang chờ này, đưa nó từ nhóm công việc đến bộ nhớ chính và bắt đầu thực thi nó. Bộ xử lý thực hiện một công việc cho đến khi nó bị gián đoạn bởi một số yếu tố bên ngoài hoặc nó thực hiện một tác vụ I / O.
Hệ thống không đa chương trình đang hoạt động:
Trong một hệ thống không đa lập trình, Ngay sau khi một công việc rời khỏi CPU và thực hiện một số tác vụ khác (ví dụ I / O), CPU sẽ trở nên nhàn rỗi. CPU tiếp tục chờ và đợi cho đến khi công việc này (đã được thực thi trước đó) quay trở lại và tiếp tục thực hiện với CPU. Vì vậy, CPU vẫn miễn phí trong tất cả thời gian này.
Bây giờ nó có một nhược điểm là CPU không hoạt động trong một thời gian rất dài. Ngoài ra, các công việc khác đang chờ thực hiện có thể không có cơ hội thực hiện vì CPU vẫn được phân bổ cho công việc trước đó. Điều này đặt ra một vấn đề rất nghiêm trọng là mặc dù các công việc khác đã sẵn sàng thực thi, CPU không được cấp phát cho chúng vì CPU được phân bổ cho một công việc thậm chí không sử dụng nó (vì nó đang bận trong các tác vụ I / O).
Không thể xảy ra trường hợp một công việc sử dụng CPU trong 1 giờ trong khi những công việc khác đã chờ đợi trong hàng đợi trong 5 giờ. Để tránh những tình huống như thế này và sử dụng CPU hiệu quả, người ta đã đưa ra khái niệm đa lập trình.
Nhiều hệ thống được chương trình đang hoạt động
Trong một hệ thống đa lập trình, ngay sau khi một công việc thực hiện một tác vụ I / O, hệ điều hành sẽ ngắt công việc đó, chọn một công việc khác từ nhóm công việc (hàng đợi), cung cấp cho CPU công việc mới này và bắt đầu thực thi nó. Công việc trước đó tiếp tục thực hiện hoạt động I / O của nó trong khi công việc mới này thực hiện các tác vụ ràng buộc CPU. Bây giờ giả sử công việc thứ hai cũng áp dụng cho một tác vụ I / O, CPU chọn công việc thứ ba và bắt đầu thực thi nó. Ngay sau khi một công việc hoàn thành hoạt động I / O của nó và quay trở lại với các tác vụ của CPU, CPU sẽ được cấp phát cho nó.
Bằng cách này, không có thời gian CPU bị lãng phí bởi hệ thống chờ đợi tác vụ I / O được hoàn thành. Do đó, mục tiêu cuối cùng của lập trình đa là giữ cho CPU luôn bận rộn miễn là có các tiến trình sẵn sàng thực thi. Bằng cách này, nhiều chương trình có thể được thực thi trên một bộ xử lý bằng cách thực thi một phần của chương trình tại một thời điểm, một phần của chương trình khác sau phần này, sau đó là một phần của chương trình khác, v.v., do đó thực thi nhiều chương trình. Do đó, CPU không bao giờ ngừng hoạt động.
Trong hình dưới đây, chương trình A chạy một thời gian rồi chuyển sang trạng thái chờ. Trong thời gian trung bình, chương trình B bắt đầu thực hiện. Vì vậy, CPU không lãng phí tài nguyên của nó và tạo cơ hội cho chương trình B chạy.
2. Multiprocessing
Trong hệ thống đơn bộ xử lý, chỉ có một quá trình thực thi tại một thời điểm.
Đa xử lý là việc sử dụng hai hoặc nhiều CPU (bộ xử lý) trong một hệ thống máy tính. Thuật ngữ này cũng đề cập đến khả năng của một hệ thống hỗ trợ nhiều hơn một bộ xử lý trong một hệ thống máy tính duy nhất. Bây giờ vì có nhiều bộ xử lý có sẵn, nhiều quá trình có thể được thực hiện cùng một lúc. Các bộ xử lý đa này chia sẻ bus máy tính, đôi khi cả đồng hồ, bộ nhớ và các thiết bị ngoại vi.
Nhiều hệ thống xử lý đang hoạt động:
Với sự trợ giúp của đa xử lý, nhiều quá trình có thể được thực hiện đồng thời. Giả sử các quá trình P1, P2, P3 và P4 đang chờ thực thi. Bây giờ trong một hệ thống xử lý duy nhất, trước hết một quá trình sẽ thực thi, sau đó là quá trình kia, sau đó là quá trình khác, v.v.
Nhưng với đa xử lý, mỗi quá trình có thể được gán cho một bộ xử lý khác nhau để thực hiện. Nếu nó là bộ xử lý lõi kép (2 bộ xử lý), hai quá trình có thể được thực thi đồng thời và do đó sẽ nhanh hơn hai lần, tương tự bộ vi xử lý lõi tứ sẽ nhanh hơn bốn lần so với bộ xử lý đơn.
Tại sao sử dụng đa xử lý?
Ưu điểm chính của hệ thống đa xử lý là hoàn thành nhiều công việc hơn trong một khoảng thời gian ngắn hơn. Các loại hệ thống này được sử dụng khi yêu cầu tốc độ rất cao để xử lý một khối lượng lớn dữ liệu. Hệ thống đa xử lý có thể tiết kiệm tiền so với hệ thống xử lý đơn vì các bộ xử lý có thể dùng chung thiết bị ngoại vi và nguồn cung cấp năng lượng.
Nó cũng cung cấp độ tin cậy cao hơn theo nghĩa là nếu một bộ xử lý bị lỗi, công việc sẽ không dừng lại mà chỉ chậm lại. ví dụ. nếu chúng ta có 10 bộ vi xử lý và 1 bộ xử lý bị lỗi, thì công việc sẽ không dừng lại, đúng hơn là 9 bộ vi xử lý còn lại có thể chia sẻ công việc của bộ xử lý thứ 10. Do đó, toàn bộ hệ thống chỉ chạy chậm hơn 10 phần trăm, chứ không phải là thất bại hoàn toàn.
Đa xử lý đề cập đến phần cứng (tức là các đơn vị CPU) chứ không phải phần mềm (tức là các quy trình đang chạy). Nếu phần cứng bên dưới cung cấp nhiều hơn một bộ xử lý thì đó là quá trình đa xử lý. Đó là khả năng của hệ thống tận dụng sức mạnh tính toán của nhiều bộ xử lý.
Sự khác biệt giữa đa chương trình và da xử lý
Một hệ thống có thể được lập trình đa bằng cách có nhiều chương trình chạy cùng lúc và đa xử lý bằng cách có nhiều hơn một bộ xử lý vật lý. Sự khác biệt giữa đa xử lý và đa lập trình là Đa xử lý về cơ bản là thực hiện nhiều quy trình cùng lúc trên nhiều bộ xử lý, trong khi đa lập trình là lưu giữ một số chương trình trong bộ nhớ chính và thực thi chúng đồng thời chỉ bằng một CPU duy nhất.
Đa xử lý xảy ra bằng cách xử lý song song trong khi Đa lập trình xảy ra bằng cách chuyển từ quá trình này sang quá trình khác (hiện tượng được gọi là chuyển mạch ngữ cảnh).
3. Multitasking
Như tên gọi của chính nó, đa tác vụ đề cập đến việc thực thi nhiều tác vụ (chẳng hạn như quy trình, chương trình, luồng, v.v.) tại một thời điểm. Trong các hệ điều hành hiện đại, chúng ta có thể phát nhạc MP3, chỉnh sửa tài liệu trong Microsoft Word và lướt Google Chrome đồng thời, điều này được thực hiện nhờ đa tác vụ.
Đa nhiệm là một phần mở rộng hợp lý của đa lập trình. Cách chính mà đa nhiệm khác với đa lập trình là lập trình đa hoạt động chỉ dựa trên khái niệm chuyển đổi ngữ cảnh trong khi đa nhiệm dựa trên chia sẻ thời gian cùng với khái niệm chuyển đổi ngữ cảnh.
Hệ thống đa nhiệm đang hoạt động:
Trong một hệ thống chia sẻ thời gian, mỗi quá trình được ấn định một số lượng thời gian cụ thể để một quá trình thực thi. Giả sử có 4 quy trình P1, P2, P3 và P4 sẵn sàng thực thi. Vì vậy, mỗi người trong số họ được gán một số lượng tử thời gian mà chúng sẽ thực thi, ví dụ lượng tử thời gian 5 nano giây (5 ns). Khi một quá trình bắt đầu thực hiện (ví dụ P2), nó sẽ thực thi trong lượng tử thời gian đó (5 ns). Sau 5 ns, CPU bắt đầu thực hiện quá trình khác (ví dụ P3) trong lượng tử thời gian được chỉ định.
Do đó, CPU làm cho các tiến trình chia sẻ các lát thời gian giữa chúng và thực thi tương ứng. Ngay sau khi lượng tử thời gian của một quá trình hết hạn, một quá trình khác sẽ bắt đầu thực hiện.
Ở đây về cơ bản cũng có một chuyển đổi ngữ cảnh đang xảy ra nhưng nó xảy ra quá nhanh đến mức người dùng có thể tương tác với từng chương trình riêng biệt trong khi nó đang chạy. Bằng cách này, người dùng có ảo tưởng rằng nhiều quy trình / tác vụ đang thực hiện đồng thời. Nhưng trên thực tế, chỉ có một tiến trình / tác vụ được thực thi tại một thời điểm cụ thể. Trong đa nhiệm, chia sẻ thời gian được thể hiện tốt nhất bởi vì mỗi quá trình đang chạy chỉ chiếm một lượng tử thời gian hợp lý của CPU.
Nói một cách khái quát hơn, đa nhiệm là việc có nhiều chương trình, tiến trình, tác vụ, luồng chạy cùng một lúc. Thuật ngữ này được sử dụng trong các hệ điều hành hiện đại khi nhiều tác vụ chia sẻ một tài nguyên xử lý chung (ví dụ: CPU và Bộ nhớ).
Như được mô tả trong hình trên, Tại bất kỳ thời điểm nào CPU chỉ thực hiện một tác vụ trong khi các tác vụ khác đang chờ đến lượt. Ảo tưởng về sự song song đạt được khi CPU được phân công lại cho một nhiệm vụ khác. tức là tất cả ba nhiệm vụ A, B và C dường như xảy ra đồng thời vì chia sẻ thời gian.
Vì vậy, để đa nhiệm diễn ra, trước tiên phải có đa chương trình, tức là sự hiện diện của nhiều chương trình sẵn sàng để thực thi. Và thứ hai là khái niệm chia sẻ thời gian.
4. Multithreading
Một luồng là một đơn vị cơ bản của việc sử dụng CPU. Đa luồng là một mô hình thực thi cho phép một quá trình có nhiều đoạn mã (tức là các luồng) chạy đồng thời trong “ngữ cảnh” của quá trình đó.
Ví dụ. Trình phát phương tiện VLC, trong đó một luồng được sử dụng để mở trình phát phương tiện VLC, một luồng để phát một bài hát cụ thể và một luồng khác để thêm các bài hát mới vào danh sách phát.
Đa luồng là khả năng của một quá trình quản lý việc sử dụng nó bởi nhiều người dùng tại một thời điểm và quản lý nhiều yêu cầu của cùng một người dùng mà không cần phải có nhiều bản sao của chương trình.
Hệ thống đa luồng đang hoạt động:
Trường hợp 1:
Giả sử có một máy chủ web xử lý các yêu cầu của khách hàng. Bây giờ nếu nó thực thi như một quy trình đơn luồng, thì nó sẽ không thể xử lý nhiều yêu cầu cùng một lúc. Đầu tiên, một máy khách sẽ đưa ra yêu cầu của mình và hoàn thành việc thực thi và chỉ sau đó, máy chủ mới có thể xử lý yêu cầu của một máy khách khác. Đây là một công việc thực sự tốn kém, mất thời gian và mệt mỏi. Để tránh điều này, có thể sử dụng đa luồng.
Bây giờ, bất cứ khi nào có yêu cầu của khách hàng mới, máy chủ web chỉ cần tạo một luồng mới để xử lý yêu cầu này và tiếp tục thực thi để nghe thêm các yêu cầu của khách hàng. Vì vậy máy chủ web có nhiệm vụ lắng nghe các yêu cầu mới của khách hàng và tạo các luồng cho từng yêu cầu riêng lẻ. Mỗi luồng mới được tạo xử lý một yêu cầu của khách hàng, do đó giảm bớt gánh nặng cho máy chủ web.
Trường hợp 2:
Chúng ta có thể coi các luồng là các quy trình con chia sẻ tài nguyên quy trình mẹ nhưng thực thi độc lập. Bây giờ hãy lấy trường hợp của GUI. Giả sử chúng tôi đang thực hiện một phép tính trên GUI (mất rất nhiều thời gian để hoàn thành). Bây giờ chúng ta không thể tương tác với phần còn lại của GUI cho đến khi lệnh này kết thúc quá trình thực thi. Để có thể tương tác với phần còn lại của GUI, lệnh tính toán này nên được gán cho một luồng riêng biệt. Vì vậy, tại thời điểm này, 2 luồng sẽ thực thi, tức là một để tính toán và một cho phần còn lại của GUI. Do đó, ở đây trong một quy trình duy nhất, chúng tôi đã sử dụng nhiều luồng cho nhiều chức năng.
Hình ảnh dưới đây mô tả hoàn toàn ví dụ về trình phát VLC:
Ưu điểm của đa luồng
Lợi ích của Đa luồng bao gồm tăng khả năng phản hồi. Vì có nhiều luồng trong một chương trình, vì vậy nếu một luồng mất quá nhiều thời gian để thực thi hoặc nếu nó bị chặn, phần còn lại của luồng sẽ tiếp tục thực thi mà không gặp bất kỳ sự cố nào. Do đó, toàn bộ chương trình vẫn đáp ứng cho người dùng bằng các luồng còn lại.
Một ưu điểm khác của đa luồng là nó ít tốn kém hơn. Tạo các quy trình hoàn toàn mới và phân bổ tài nguyên là một công việc tốn nhiều thời gian, nhưng vì các luồng chia sẻ tài nguyên của quy trình mẹ nên việc tạo các luồng và chuyển đổi giữa chúng tương đối dễ dàng. Do đó đa luồng là nhu cầu của các Hệ điều hành hiện đại.
Bạn sẽ nhập lớp này vào main.dart.(Chúng tôi sẽ thực hiện việc này ở bước sau 3.) Mã của lớp này ở cuối bài viết này.
3. Thực hiện một số thao tác khởi tạo
trong main()
4. Chức năng hiển thị cuộc gọi trực tiếp ở bất cứ đâu khi bạn muốn
showNotification()là một hàm của lớp NotificationService mà chúng ta vừa tạo trong tệp notification_service.dart.
Ví dụ: tôi muốn thông báo đẩy khi tôi nhấp vào một nút, sau đó tôi có thể gọi chức năng này trong _onPressed()nút. Mã ở dưới đây, hãy kiểm tra nó.
main.dart
import 'package:flutter/material.dart';
import 'service/notification_service.dart';
Future<void> main() async{
// Step 3. Initialization
WidgetsFlutterBinding.ensureInitialized();
NotificationService notificationService = NotificationService();
await notificationService.init();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
/// Step 4. For pusing notification.
NotificationService notificationService = NotificationService();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("My Main Page"),
),
body: const Text('hello world'),
floatingActionButton: FloatingActionButton(
onPressed: () async{
/// When I click the button, the notification will be pushed.
/// Step 4. just call this function anywhere you want.
await notificationService.showNotification(0, 'This is title...', "Tis is body...",);
},
child: const Icon(Icons.radio_button_on),
),
);
}
}
notification_service.dart
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
/// Step 2. Create a NotificationService class
class NotificationService {
// Singleton pattern, https://en.wikipedia.org/wiki/Singleton_pattern
// 1.The _internal() construction is just a name often given to constructors
// that are private to the class
// 2.Use the factory keyword when implementing a constructor
// that does not create a new instance of its class.
NotificationService._internal();
static final NotificationService _notificationService = NotificationService._internal();
factory NotificationService() {
return _notificationService;
}
// Configuration for platform-specific initialization settings.
Future<void> init() async {
// Specifies the default icon for notifications.
// icon path: PROJECT_NAME\android\app\src\main\res\drawable\icon.png
const AndroidInitializationSettings androidInitializationSettings =
AndroidInitializationSettings("icon");
const InitializationSettings initializationSettings =
InitializationSettings(android: androidInitializationSettings);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
}
// Some configurations for platform-specific notification's details.
static const AndroidNotificationDetails _androidNotificationDetails =
AndroidNotificationDetails(
'ChannelId',
'ChannelName',
channelDescription: "Responsible for all local notifications",
playSound: true,
priority: Priority.high,
importance: Importance.high,
);
final NotificationDetails notificationDetails =
const NotificationDetails(android: _androidNotificationDetails);
// Now we need to call the show() method of FlutterLocalNotificationsPlugin.
// Show() is responsible for showing the local notification.
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Future<void> showNotification(int id, String title, String body) async {
await flutterLocalNotificationsPlugin.show(id, title, body, notificationDetails);
}
}