Blog

  • Cách viết Unit Test trong Flutter

    [FLUTTER] Cách viết Unit Test trong Flutter (Phần 1)

    Lời ngỏ

    Unit Test là một phần rất quan trọng trong quá trình phát triển phần mềm, tuy nhiên nó thường xuyên bị lãng quên với một lập trình viên mới vào nghề hoặc chưa có nhiều kinh nghiệm. Mong rằng bài viết sẽ giúp bạn có cái nhìn trực quan hơn về Unit Test tron phát triển phầm mềm, đặc biệt là trong Flutter.

    A. Đôi điều về Unit Test

    1. Unit Test là gì?

    Unit Test là gì? Tìm hiểu chi tiết về Unit Test
    1.1 Ảnh unit test

    Unit Test là một loại kiểm thử phần mềm trong đó các đơn vị hay thành phần riêng lẻ của phần mềm được kiểm thử. Kiểm thử đơn vị được thực hiện trong quá trình phát triển ứng dụng. Mục tiêu của Kiểm thử đơn vị là cô lập một phần code và xác minh tính chính xác của đơn vị đó.

    Nếu khái niệm trên vẫn còn khá khó hiểu thì hãy thử tách nghĩa từng từ ra một nhé:

    • Unit là một thành phần Phần mềm nhỏ nhất mà ta có thể kiểm tra được như các hàm (Function), thủ tục (Procedure), lớp (Class), hoặc các phương thức (Method).
    • Test thì là kiểm thử, kiểm tra tính chính xác của một cái gì đó.

    Đến đây thì chắc các bạn cũng đã có cho mình một chút khái niệm về Unit Test rồi đúng không nhỉ.

    Thường các lập trình viên khi nghe về một khái niệm mà trong đó có từ “Test” thì điều mọi người sẽ nghĩ đến ngay đó là “Test là công việc của Tester đâu phải việc của mình nên mình không cần quan tâm :v”.

    Nhưng KHÔNG, các bạn đã nhầm to. Unit Test sẽ phải được viết bởi các lập trình viên, bởi chính những người viết ra những dòng code đó.

    2. Vòng đời của Unit Test

    Vòng đời của Unit Test gồm 3 giai đoạn:

    • Fail (trạng thái lỗi).
    • Ignore (tạm ngừng thực hiện).
    • Pass (trạng thái làm việc).

    Ba giai đoạn này sẽ thay phiên nhau làm việc khi một Unit Test được chạy tự động.

    3. Unit Test quan trọng không và khi nào thì cần viết Unit Test?

    Spring Break

    Unit Test là một phần không thể thiếu trong quá trình phát triển phần mềm. Unit Test đem lại cho chúng ta rất nhiều lợi ích:

    • Tạo ra một môi trường để kiểm tra bất kỳ đoạn code nào, duy trì sự ổn định của phần mềm. Unit Test giúp chúng ra kiểm tra những kết quả trả về mong muốn cũng như những ngoại lệ mong muốn.
    • Phát hiện các lỗi, các xử lý không hiệu quả trong code, các vấn đề về thiết kế.
    • Việc viết Unit Test có thể coi như việc tạo một người dùng đầu tiên cho ứng dụng, từ đó chúng ta có thể biết được những vấn đề mà trong quá trình sử dụng ứng dụng người dùng có thể gặp phải.
    • Giúp cho quá trình phát triển phần mềm trở nên nhanh hơn, số lượng test case khi được test cũng sẽ pass nhiều hơn. Điều này giúp cho các bộ phận khác như QA, Tester làm việc sẽ nhàn hơn. Và trên hết đối với những coder chúng ta, việc ít phải đối mặt với Tester cũng làm cho buổi làm việc “bớt sóng gió” hơn đúng không nào?

    Note

    Viết Unit Test càng sớm càng tốt trong giai đoạn viết code và xuyên suốt chu kỳ Phát triển phần mềm.

    4. Như nào là một Unit Test có giá trị?

    Muốn viết một Unit Test hiệu quả, đem lại nhiều lợi ích nhất cho bản thân cũng như dự án thì cần chú ý những điểm sau:

    • Unit Test chạy nhanh, sử dụng dữ liệu dễ hiểu, dễ đọc.
    • Hãy làm cho mỗi test độc lập với những phần khác. Mỗi test chỉ nên liên quan đến một hàm, thủ tục, … Điều này sẽ giúp chúng ta dễ dàng hơn trong quá trình quản lý unit test, cũng như đáp ứng được các thay đổi trong code.
    • Giả lập tất cả các dịch vụ và trạng thái bên ngoài. Ví dụ: Nếu bạn có làm việc với Database, thì KHÔNG nên sử dụng database thật của ứng dụng để viết Unit Test, bởi vì giá trị trong đó sẽ có thể thay đổi và ảnh hưởng đến các kết quả mong đợi của bạn. Thay vào đó hay tự vào cho mình một fake database, với dự liệu có sẵn và chỉ sử dụng các hàm, thủ tục để làm việc với nó.
    • Nên đặt tên các đơn vị kiểm thử rõ ràng và nhất quán với nhau để đảm bảo rằng test case dễ đọc. Để bất kỳ ai cũng có thể khởi chạy test case mà không gặp phải trở ngại.
    • Triển khai Unit Test bao quát hết tất cả các ngoại lệ, các test case.

    B. Cách triển khai Unit Test trong Flutter (Còn tiếp)

    Nguồn tham khảo:

    Tác giả: LamNT59

  • Flutter Unit test: Mock dependencies using Mockito

    1. Tại sao phải sử dụng Mockito.

    Các unit tests có thể sẽ phụ thuộc vào các class có fetch data từ web và cơ sở dữ liệu. Điều này bất tiền do một vài lý do:

    • Việc fetch data từ web và cơ sở dữ liệu về sẽ chậm để thực hiện kiểm tra.
    • Việc kiểm tra có thể bắt đầu lỗi nếu kết quả từ web và cơ sở dữ liệu là kết quả không được mong đợi. Điều này gọi là “flaky test”.
    • Khó để kiểm tra tất cả các trường hợp thành công hay lỗi các hành động sử dụng web và cơ sở dữ liệu.

    Vì vậy, chúng ta có thể sử dụng “mock” dependencies. Mocks cho phép mô phỏng web và cơ sở dữ liệu và trả về kết quả cụ thể tùy thuộc vào tình huống.

    2. Các bước sử dụng Mockito package để kiểm tra.

    Các bước bao gồm:

    1. Thêm các package vào pubspec.yaml.
    2. Tạo function để kiểm tra.
    3. Tạo file test với mock http.Client.
    4. Viết các test cho mỗi điều kiện.
    5. Chạy kiểm tra.

    2.1. Thêm các package dependencies vào pubspec.yaml.

    Thêm các package dependencies: http, mockito, build_runner với version mới nhất ( tham khảo trên trang pub.dev) vào file pubspec.yaml.

    2.2. Tạo function để kiểm tra.

    Ở ví dụ này, chúng ta tạo một unit test fetchAlbum function để lấy dữ liệu từ internet. Để kiểm tra function này, chúng ta cần 2 thay đổi đó là:

    1. Tham số của function phải có http.Client. Điều này cho phép cung cấp chính xác http.Client phụ thuộc trong trường hợp. Đối với Flutter và dự án web và cơ sở dữ liệu-side cung cấp http.IOClient. Đối với Browse apps, cung cấp http.BrowserClient. Đối với tests, cung cấp mock http.Client.
    2. Sử dụng client để lấy dữ liệu từ internet điều này tốt hơn là phương pháp http.get() trong việc làm giả.

    2.3. Tạo file test với mock http.Client.

    Tạo file fetch_album_test.dart trong folder test.

    Thêm annotation @GenerateMocks([http.Client]) để main function tạo ra MockClient class với mockito. MockClient class được tạo ra sẽ thể hiện http.Client class. Điều này cho phép chúng ta có thể thực hiện fetchAlbum function và trả lại các kết quả khác nhau cho mỗi trường hợp test trong MockClient.

    Các mocks được sinh ra sẽ được để trong .

    Sau đó, trong command chạy: flutter pub run build_runner build để sinh ra mocks.

    2.4. Viết các test cho mỗi điều kiện.

    Ở ví dụ này, fetchAlbum() function sẽ trả về:

    1. Nếu http gọi thành công sẽ trả về Album()
    2. Nếu lỗi thì sẽ trả về Exception.

    2.5. Chạy file tests.

    Chúng ta chỉ việc ấn Run trong file tests trong thư mục test: …/test/fetch_album_test.dart.

    3. Tổng kết

    Trên đây là một số chia sẻ về việc sử dụng Unit test: Mock dependencies using Mockito cơ bản giúp cho việc kiểm tra các function thao tác với web và cơ sở dữ liệu dễ dàng hơn. Mong rằng qua bài viết sẽ giúp ích cho các bạn phần nào đó.

    Cảm ơn các bạn đã đọc bài viết của mình.

    Source code: https://github.com/falcon12795/unit_test

    Tài liệu tham khảo: https://docs.flutter.dev/cookbook/testing/unit/mocking

    Author: DuongVT19

  • Kiến thức cơ bản về Dart(Phần 1)

    Giới thiệu

    Giới thiệu về kiến ​​thức cơ bản của ngôn ngữ lập trình Dart, được sử dụng để phát triển với Flutter SDK dành cho thiết bị di động, web và hơn thế nữa.

    Flutter là một bộ công cụ giao diện người dùng thú vị của Google cho phép bạn viết ứng dụng cho các nền tảng khác nhau bao gồm iOS, Android, web và hơn thế nữa, tất cả đều sử dụng một cơ sở mã. Flutter sử dụng ngôn ngữ Dart.

    Nếu bạn chưa quen với Dart, hướng dẫn này sẽ giới thiệu cho bạn các khái niệm cơ bản của nó và cho bạn thấy nó tương tự như thế nào với các ngôn ngữ lập trình khác mà bạn có thể đã biết.

    Trong suốt quá trình hướng dẫn này, bạn sẽ được giới thiệu về những điều cơ bản của Dart chẳng hạn như:

    • Variables, data types, and operators
    • Conditionals and loops
    • Collections
    • Functions

    Khi bạn hoàn thành, bạn sẽ sẵn sàng đi sâu vào phát triển Flutter bằng cách sử dụng Dart.

    Bắt Đầu

    Để bắt đầu nhanh chóng, cách tốt nhất của bạn là sử dụng công cụ mã nguồn mở DartPad , cho phép bạn viết và kiểm tra mã Dart thông qua trình duyệt web:

    DartPad được thiết lập giống như một IDE thông thường. Nó bao gồm các thành phần sau:

    • Khung trình soạn thảo : Nằm ở bên trái. Mã của bạn sẽ xuất hiện ở đây.
    • Nút RUN : Chạy mã trong trình chỉnh sửa.
    • Bảng điều khiển : Nằm ở phía trên bên phải, bảng này hiển thị đầu ra.
    • Bảng tài liệu : Nằm ở dưới cùng bên phải, bảng này hiển thị thông tin về mã.
    • Samples: Trình đơn thả xuống này hiển thị một số mã mẫu.
    • Nút Null Safety: Sử dụng nút này để chọn tham gia vào tính năng an toàn không có âm thanh mới của Dart.
    • Thông tin phiên bản : Ở phía dưới cùng bên phải, DartPad hiển thị phiên bản Flutter và Dart mà nó hiện đang sử dụng.

    Nếu muốn, bạn có thể cài đặt Dart SDK cục bộ trên máy của mình. Một cách để làm như vậy là cài đặt Flutter SDK . Cài đặt Flutter cũng sẽ cài đặt Dart SDK.

    Để cài đặt trực tiếp SDK Dart, hãy truy cập https://dart.dev/get-dart .

    Tại sao nên chọn Dart

    Dart có nhiều điểm tương đồng với các ngôn ngữ khác như Java, C #, Swift và Kotlin. Một số tính năng của nó bao gồm:

    • Statically typed
    • Type inference
    • String expressions
    • Multi-paradigm including object-oriented and functional programming
    • Null safe

    Dart được tối ưu hóa để phát triển các ứng dụng nhanh trên nhiều nền tảng.

    Variables, Comments and Data Types

    Điều đầu tiên bạn sẽ thêm vào main là một câu lệnh gán biến. Các biến giữ dữ liệu mà chương trình của bạn sẽ hoạt động.

    Bạn có thể coi một biến như một hộp trong bộ nhớ máy tính của bạn chứa một giá trị. Mỗi hộp có một tên, đó là tên của biến. Để biểu thị một biến bằng Dart, hãy sử dụng var từ khóa.

    Thêm một biến mới vào main:

    var myAge = 35 ;  
    
    

    Mỗi câu lệnh Dart kết thúc bằng dấu chấm phẩy, giống như các câu lệnh trong C và Java. Trong đoạn mã trên, bạn đã tạo một biến myAge và đặt nó bằng 35 .

    Bạn có thể sử dụng print Dart tích hợp sẵn để in biến vào bảng điều khiển. Thêm lệnh gọi đó sau biến:

    in (myAge); // 35
    
    

    Nhấp vào RUN trong DartPad để chạy mã. Bạn sẽ thấy giá trị của biến 35 được in trong bảng điều khiển.

    Comments

    Comments trong Dart giống như trong C và các ngôn ngữ khác. Văn bản sau // là một nhận xét một dòng, trong khi văn bản bên trong /* ... */ là một khối nhận xét nhiều dòng.

    Đây là một ví dụ:

    // Đây là một nhận xét một dòng. 
    
    in (myAge); // Đây cũng là một nhận xét một dòng. 
    
    / * 
     Đây là một khối bình luận nhiều dòng. Điều này rất hữu ích cho những 
     bình luận dài kéo dài vài dòng. 
     * /
    
    

    Data Types

    Dart là statically typed , nghĩa là mỗi biến trong Dart có một kiểu mà bạn phải biết khi biên dịch mã. Loại biến không thể thay đổi khi bạn chạy chương trình. C, Java, Swift và Kotlin cũng có statically typed .

    Điều này trái ngược với các ngôn ngữ như Python và JavaScript, được gõ động . Điều đó có nghĩa là các biến có thể chứa các loại dữ liệu khác nhau khi bạn chạy chương trình. Bạn không cần biết kiểu khi bạn biên dịch mã.

    Nhấp vào myAge cửa sổ trình chỉnh sửa và tìm trong bảng Tài liệu . Bạn sẽ thấy Dart được suy ra là myAge kiểu int vì nó được khởi tạo với giá trị số nguyên 35 .

    Nếu bạn không chỉ định rõ ràng một kiểu dữ liệu, Dart sẽ sử dụng suy luận kiểu để cố gắng xác định nó, giống như Swift và Kotlin.

    Dart cũng sử dụng kiểu suy luận cho các kiểu khác int. Nhập một biến pi bằng 3,14:

    var pi = 3,14 ; in (pi); // 3,14
    
    
    
    

    Dart pi được cho là double bởi vì bạn đã sử dụng một giá trị dấu phẩy động để khởi tạo nó. Bạn có thể xác minh điều đó trong bảng thông tin Dart bằng cách nhấp vào pi.

    Các kiểu dữ liệu cơ bản

    Dart sử dụng các loại cơ bản sau:

    • int : Số nguyên
    • double : Số phức
    • bool : Booleans
    • String: Chuỗi ký tự

    Dưới đây là một ví dụ về từng loại trong Dart:

    int và double cả hai đều bắt nguồn từ một kiểu được đặt tên numnum sử dụng dynamic từ khóa để bắt chước cách nhập động trong Dart.

    Thực hiện việc này bằng cách thay thế varbằng loại bạn muốn sử dụng:

    int yourAge = 27 ;
    
    in (yourAge); // 27
    
    

    Từ khóa Dynamic

    Nếu bạn sử dụng từ khóa dynamic từ khóa thay vì var, bạn sẽ nhận được một biến được nhập động một cách hiệu quả:

    dynamic numberOfKittens;
    
    

    Tại đây, bạn có thể đặt numberOfKittensthành một String dấu ngoặc kép. Bạn sẽ tìm hiểu thêm về String loại này sau trong hướng dẫn.

    numberOfKittens = 'There are no kittens!';
    
    print(numberOfKittens); // There are no kittens!
    
    

    numberOfKittenscó một kiểu, vì Dart có kiểu gõ tĩnh. Nhưng kiểu dynamicđó, có nghĩa là bạn có thể gán các giá trị khác với các kiểu khác cho nó. Vì vậy, bạn có thể chỉ định một intgiá trị bên dưới câu lệnh in của mình.

    numberOfKittens = 0 ; print (numberOfKittens); // 0
    
    
    
    

    Hoặc, nếu bạn có một con mèo con trong hộp của Schrödinger , bạn có thể gán một double giá trị:

    numberOfKittens = 0,5 ; print (numberOfKittens); // 0,5
    
    
    
    
    

    Nhấp vào RUN để xem ba giá trị khác nhau numberOfKittens được in trong bảng điều khiển. Trong mỗi trường hợp, kiểu của numberOfKittens phần còn lại dynamic, mặc dù bản thân biến đó giữ các giá trị của các kiểu khác nhau.

    Booleans

    Kiểu bool chứa các giá trị của một trong hai true hoặc false.

    bool areThereKittens = false ; print (areThereKittens); // false
    
    
    
    

    Nhưng, nếu bạn nhìn vào bên trong hộp của Schrödinger, bạn có thể chuyển sang có một con mèo con thực sự có:

    numberOfKittens = 1 ; 
    
    areThereKittens = true ; print (areThereKittens); // true
    
    
    
    

    Chạy lại mã để xem các giá trị Boolean của bạn trong bảng điều khiển. 

  • Bắt đầu với RxSwift ( Phần 1)

    Bắt đầu với RxSwift ( Phần 1)

    I. Giới thiệu

    Một trong những điều quan trọng của lập trình hướng đối tượng (OOP) và hướng thủ tục đó là imperative (lập trình mệnh lệnh). Chúng ta cần sử dụng những câu lệnh để thay đổi trạng thái của chương trình.

    Vậy câu hỏi đặt ra làm sao để trạng thái của chương trình có thể thay đổi một cách tự động, liệu ngôn ngữ, khái niệm lập trình nào làm được việc đó không ?

    → Câu trả lời đó là reactive programming.

    Khi mà ứng dụng của bạn phản ứng lại với những thay đổi của data, reactive programming sẽ giúp chúng ta thực hiện điều đó. Nó giúp chúng ta tâp trung vào việc xử lí logic và không cần quan tâm tới việc thay đổi giữa các trạng thái (state) với nhau.

    Chúng ta có thấy trong swift sử dụng KVO và didSet để thiết lập cho lập trình phản ứng (reactive), nhưng việc thiết lập cho dữ liệu lớn, hoặc bài toán phức tạp hơn khá rắc rối. Vậy nên chung ta sẽ dùng tới thư viện thứ 3 đó là RxSwift

    Note*: “KVO là một khái niệm chúng ta sẽ gặp nhiều khi sử dụng SwiftUI, các bạn có thể search thêm về khái niệm này nhé.”

    Thư viện RxSwift sẽ giúp chúng ta giải quyết vấn đề trên qua lập trình bất đồng bộ (asynchronous programming).

    Để không tốn thời gian của các bạn mình sẽ đi nhanh qua các thành phần mà RxSwift cung cấp

    II. Các thành thần (components) chính của RxSwift:

    • Observable và Observer
    • Subject
    • DisposeBag
    • Operators
    • Schedules

    Observable và Observer

    Có rất nhiều thuật ngữ để mô tả cho lập trình bất đồng bộ, ở đâu mình sẽ sử dụng thuật ngữ observebal và Observer luôn nhé.

    • Observer lắng nghe Observable.
    • Observable phát ra các items hoặc gửi các notifications đến các Observer bằng cách gọi các Observer methods.
    Khái niệm Observable đến từ observer design pattern là một đối tượng thông báo cho các đối tượng theo dõi về một điều gì đó đang diễn ra

    • Một Observer đăng ký lắng nghe một Observable, sau đó nó sẽ xử lý một item hoặc nhiều các item mà Observable phát ra.

    Chúng ta có thể đăng kí tới một Observable sequence thông qua subscribe(on:(Event<T>)->()).

    Trong RxSwift một sự kiện sẽ chỉ là một trong Enumeration Type với 3 trạng thái có thể xảy ra:

    .onNext(value: T) -> Observable  gọi hàm onNext  có tham số là item, item này là một trong các tập items của Observable

    .onError(error: Error) -> Được gọi khi Observable  kết thúc với một lỗi xảy ra trong quá trình chuyển đổi, xử lý dữ liệu.

    .onCompleted -> Observable  gọi hàm này sau khi hàm onNext  cuối cùng được gọi, nếu không có bất kì lỗi nào xảy ra.

    Ví dụ với code Swift: 

    let obj = Observable.from(["A", "B", "C", "D"]) // Khởi tạo một Observable
    obj.subscribe( // Thực hiện subscribe Observable
      onNext: { data in
        print(data) // Nơi nhận dữ liệu của Observer được gửi đi từ Observable
      }, 
      onError: { error in
        print(error) // Nơi nhận error và Observable được giải phóng
      }, 
      onCompleted: {
        print("Completed") // Nhận được sự kiện khi Observable hoàn thành và Observable được giải phóng
      })
       .disposed()
    

    Kết quả trả về:

    A

    B

    C

    D

    Competed

    Subject

    Một đối tượng vừa có thể là Observable vừa có thể là Observer được gọi là Subject.

    Trong RxSwift cung cấp cho chúng ta 4 subject khác nhau với cách thứ hoạt động khác nhau đó là:

    • PublishSubject: Khởi đầu “empty” và chỉ emit các element mới cho subscriber của nó.
    • BehaviorSubject: Khởi đầu với một giá trí khởi tạo và sẽ relay lại element cuối cùng của chuỗi cho Subscriber mới.
    • ReplaySubject: Khởi tạo với một kích thước bộ đệm cố định, sau đó sẽ lưu trữ các element gần nhất vào bộ đệm này và relay lại các element chứa trong bộ đệm cho một Subscriber mới.
    • BehaviourReplay (which was Variable): Lưu trữ một giá trị như một state và sẽ relay duy nhất giá trị cuối cùng cho Subscriber mới.

    Để đi sâu vào từng loại subject mà RxSwift cung cấp khá là dài, nên mình chỉ lướt qua. Các bạn có thể tìm hiểu thêm, hoặc chờ một bài viết viết của mình đi sâu phân tích các subject trên nhé!

    Ở phần 1 mình đã giới thiệu qua khái niệm Reactive Programing, và một số thành phần chính của nó là Observabel và Observer, Subject.

    Các phần tiếp theo mình sẽ gửi tới các bạn ở phần 2 nhé.

    Cảm ơn các bạn đã đọc bài viết của mình.

    CongPQ

  • Sử dụng Enum trong Enum

    1. Enum là gì?

    An enumeration defines a common type for a group of related values and enables you to work with those values in a type-safe way within your code.

    Ở đây chúng ta sẽ lấy nguyên định nghĩa mà Apple đưa ra, hiểu một các nôm na thì Enum là kiểu dữ để định nghĩa một nhóm có giá trị liên quan, từ đó giúp bạn làm việc an toàn với kiểu dữ liệu đó trong code của bạn.

    2. Enum trong Enum

    Như tiêu đề, mình sẽ đi thẳng vào việc sử dụng Enum trong Enum, các trường    hợp sử   dụng enum thông thường các bạn có thể tìm hiểu ở các blog hoặc bài viết khác.

    • Bài toán đưa ra: Khi bạn có một struct như sau:

    ở đây chúng ta thấy có một enum để định về type của các PieChart. Câu hỏi đưa ra là khi bạn vẫn muốn tiếp tục sử dụng lại struct trên cho các object tương tự, và các object đó có type khác nhau thì hướng xử lí của bạn là như thế nào?

    -> Một trong các cách mà mình nghĩ các bạn sẽ dùng tới là sử dụng lại enum, thêm case hoặc là tạo một struct và enum mới như hình:

    Ở đây chúng ta quan sát giữa strcut ChartViewEntity với ChartViewEntityInWeek. Chúng ta thấy hai struct chỉ khác nhau về type( ở đây là khác nhau vê Enum). Và giữa 2 enum lại có mối quan hệ giống nhau.

    Câu hỏi đặt ra, tại sao chúng ta không sử dụng lại cùng 1 struct mà sửa lại Enum, và trong Enum của chúng ta sẽ chưa các case là type cho ChartView trong từng trường hợp mong muốn. Và chúng ta giải quyết bài toán trên bằng cách xử lí lại như sau:

    Ở đây mình đã tạo ra 3 enum, và enum PieChartType sẽ chứa 2 Enum còn lại ( đó là các Enum con phù hợp cho từng bài toán đặt ra riêng cho mỗi Object có các thuộc tính được trìu tượng qua struct ChartViewEntity). Vậy là bài toán của chúng ta đã được giải quyết, và chúng ta nhận ra rằng việc sử dụng Enum trong Enum có hiệu quả nhất định, giúp code của bạn gọn hơn, và trở nên tường minh hơn, đảm bảo sự an toàn khi bạn viết code.

    •  Qua ví dụ trên mình đã đưa ra cách giải quyết bài toán đồng thời là ứng dụng của việc sử dụng Enum trong Enum. Vậy tạo sao chúng ta không bắt tay vào sử dụng cho các bài toán riêng của mình. Chúc các bạn thành công.

    3. Tổng kết

    Trên đây mà một số chia sẻ về việc sử dụng Enum trong Enum nói riêng, và Enum nói chung. Với Swift chúng ta có Enum giúp cho việc viết code trở nên rõ ràng và rành mạch hơn. Mong rằng qua bài viết sẽ giúp ích cho các bạn phần nào đó về việc sử dụng enum và tiến tới con đường coder chuyên nghiệp hơn.

    Cảm ơn các bạn đã đọc bài viết của mình.

    Author: CongPQ

  • Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 3

    Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 3

    Trong lập trình với Python thì Functional Programming đóng một vai trò vô cùng quan trọng và các functions trong Python là các first-class citizens. Điều đó có nghĩa là chúng ta có thể vận hành các functions giống như các objects khác:

    • Truyền các function giống như các đối số.
    • Gán một function cho một biến số.
    • Return một function từ một function khác.

    Dựa trên những điều này, Python hỗ trợ một kỹ thuật vô cùng mạnh mẽ: closures. Sau khi hiểu closures, chúng ta sẽ đi đến tiếp cận một khái niệm rất quan trọng khác – decorators. Đây là 2 khái niệm/kỹ thuật mà bất kỳ lập trình viên Python chuyên nghiệp nào cũng cần phải nắm vững.

    Trong phần 3 này, tôi sẽ giới thiệu một số ví dụ ứng dụng closure để viết code hiệu quả hơn.

    Bài viết này yêu cầu kiến thức tiên quyết về scopes, namespaces, closures trong Python. Nếu bạn chưa tự tin, thì nên đọc trước 2 bài viết dưới đây (theo thứ tự):

    Table of contents

    Closure

    Nhắc lại

    Closure có thể tránh việc lợi dụng các giá trị global và cung cấp một cách thức ẩn dữ liệu (data hiding), cung cấp một giải pháp object-oriented cho vấn đề. Khi chỉ có một vài phương thức được triển khai trong một class, thì closure có thể cung cấp một giải pháp thay thế nhưng thanh lịch hơn. Khi số lượng thuộc tính và phương thức tăng lên nhiều, thì sử dụng class sẽ phù hợp hơn. Các tiêu chí sau cho thấy closure trong Python khi một nested function có tham chiếu một giá trị trong enclosing scope:

    • Tồn tại một nested function (function bên trong function khác)
    • Nested function có tham chiếu đến một giá trị được khai báo bên trong enclosing function.
    • Enclosing function trả về nested function (giá trị được return)

    Nguồn ảnh: Andre Ye

    Averager

    Trong ví dụ này, ta sẽ xây dựng một hàm tính giá trị trung bình của nhiều giá trị sử dụng closure. Hàm này có thể tính giá trị trung bình theo thời gian bằng cách thêm các đối số vào hàm đó mà không cần phải lặp lại việc tính tổng các giá trị trước đó.

    Cách tiếp cận dễ dàng nghĩ đến nhất là sử dụng class trong Python, ở đó ta sẽ sử dụng một biến instance để lưu trữ tổng của dãy số và số số hạng. Sau đó cung cấp cho class đó một method để thêm vào 1 số hạng mới, và trả về giá trị trung bình cộng của dãy số.

    class Averager:
        def __init__(self):
            self._count = 0
            self._total = 0
    
        def add(self, value):
            self._total += value
            self._count += 1
            return self._total / self._count
    
    a = Averager()
    a.add(1) # return 1.0
    a.add(2) # return 1.5
    a.add(3) # return 2.0

    Bằng cách sử dụng closure, ta có thể tận dụng functions trong python để xây dựng được tính năng tương tự việc sử dụng class, nhưng thanh lịch và hiệu quả hơn.

    def averager():
        total = 0
        count = 0
    
        def add(value):
            nonlocal total, count
            total += value
            count += 1
            return 0 if count == 0 else total / count
    
        return add
    
    a = averager()
    a(1) # return 1.0
    a(2) # return 1.5
    a(3) # return 2.0

    Counter

    Áp dụng closure, ta có thể xây dựng 1 bộ đếm, đếm số lần gọi một function mỗi khi function đó chạy. Function này có thể nhận bất kỳ đối số hoặc đối số từ khóa nào.

    def counter(fn):
        cnt = 0  # số lần chạy fn, khởi tạo là 0
    
        def inner(*args, **kwargs):
            nonlocal cnt
            cnt = cnt + 1
            print('{0} has been called {1} times'.format(fn.__name__, cnt))
            return fn(*args, **kwargs)
    
        return inner

    Giả sử ta muốn bổ sung thêm việc đếm số lần gọi hàm tính tổng 2 số:

    def add(a, b):
        return a + b

    Ta có thể áp dụng closure như sau:

    count_sum = counter(add))
    count_sum(1, 2) # sum has been called 1 times
    count_sum(3, 5) # sum has been called 2 times

    Sở dĩ hàm count_sum có thể làm được như trên là bởi vì nó đang sở hữu 2 free variables là:

    • fn: tham chiếu đến hàm add
    • cnt: duy trì đếm số lần gọi hàm fn
    count_sum.__code__.co_freevars # ('cnt', 'fn')

    Đến đây, thay vì in ra standard output số lần gọi 1 hàm bất kỳ (hàm add chỉ là 1 ví dụ), ta có thể sử dụng 1 từ điển là global variable lưu trữ các cặp {key: value}. Ở đó, key là tên của hàm và value là số lần gọi hàm. Để làm được điều đó, ta cần sửa đổi một chút ở hàm counter bằng cách bổ sung thêm cho nó 1 đối số là tham chiếu đến từ điển lưu trữ:

    def counter(fn, counters):
        cnt = 0  # số lần chạy fn, khởi tạo là 0
    
        def inner(*args, **kwargs):
            nonlocal cnt
            cnt = cnt + 1
            counters[fn.__name__] = cnt  # counters là nonlocal
            return fn(*args, **kwargs)
    
        return inner
    func_counters = dict() # khởi tạo từ điển
    # đếm số lần chạy hàm add
    counted_add = counter(add, func_counters)
    for i in range(10):
        counted_add(i, i+1)

    Biến func_counters là biến toàn cục, vì vậy ta có thể bổ sung thêm từ khóa là tên của hàm khác vào nó, thử 1 ví dụ, xét hàm nhân 2 số:

    def mult(a, b):
        return a * b
    
    counted_mult = counter(mult, func_counters)
    for i in range(7):
        counted_mult(i, i)

    Biến func_counters lúc này sẽ cho chúng ta biết số lần gọi hàm add và số lần gọi hàm mult

    func_counters ## {'mult': 7, 'add': 10}

    Cả 2 hàm counted_addcounted_mult đều đang giữ 3 free variables:

    • fn: tham chiếu đến hàm cần đếm
    • cnt: duy trì đếm số lần gọi hàm fn
    • counters: tham chiếu đến từ điển lưu trữ thông tin về số lần đếm các hàm

    Hãy thử nghĩ, nếu như, thay vì ta gọi:

    counted_add = counter(add, func_counters)

    Ta gọi như sau:

    add = counter(add, func_counters)

    Lúc này, ta có một hàm add mới, thực sự không khác hàm add lúc đầu về tính năng là tính tổng 2 số. Tuy nhiên sau khi gọi hàm add, lúc này ta còn nhận được thêm thông tin về số lần gọi hàm add được giữ trong biến func_counters.

    Như vậy, hàm counter đóng vai trò như 1 trình trang trí cho hàm add (tương tự với hàm mult), nó bổ sung thêm tính năng cho hàm add nhưng không thay đổi hành vi của hàm ađd (trả về tổng 2 số). Đây là tính chất quan trọng của decorator mà chúng ta sẽ tìm hiểu trong một bài viết sau.

    Use Closures Skilfully

    Closure là một vũ khí mạnh mẽ của Python. Người mới bắt đầu có thể gặp đôi chút khó khăn trong việc áp dụng nó trong việc viết mã. Tuy nhiên, nếu ta có thể hiểu và sử dụng nó một cách thuần thục, thì nó sẽ vô cùng hữu ích.

    Trên thực tế thì decorator trong Python là một sự mở rộng của closure. Chúng ta sẽ bàn về decorator sau, nhưng ai cũng biết rằng hầu hết các framework sử dụng Python cho web development đều sử dụng decorator rất thường xuyên.

    Dưới đây là 2 tips quan trọng sẽ giúp bạn sử dụng closure thuần thục:

    Sử dụng lambda function để đơn giản hóa code

    Xét 1 ví dụ:

    def outer_func():
        name = "Tu Anh"
    
        def print_name():
            print(name)
    
        return print_name
    
    f = outer_func()
    print(outer_func.__closure__) # None
    print(f.__closure__) # (<cell at 0x7f31445b2e90: str object at 0x7f314459c070>,)
    print(f.__closure__[0].cell_contents) # Tu Anh

    Ta có thể làm cho ví dụ trên thanh lịch hơn bằng cách sử dụng lambda function:

    def outer_func():
        name = "Tu Anh"
    
        return lambda _: print(name)
    
    f = outer_func()
    print(outer_func.__closure__) # None
    print(f.__closure__) # (<cell at 0x7f31445a44d0: str object at 0x7f31445b6070>,)
    print(f.__closure__[0].cell_contents) # Tu Anh

    Closures che giấu các biến private hiệu quả hơn

    Trong Python thì không có các từ khóa built-in như là public hay private để kiểm soát khả năng truy cập của các biến. Theo quy ước, chúng ta sử dụng double underscores để định nghĩa một member của 1 class là private. Tuy nhiên, chúng vẫn có thể được truy cập.

    Đôi khi, chúng ta cần bảo vệ mạnh mẽ hơn để ẩn một biến. Và closures có thể giải quyết vấn đề này. Như ví dụ ở trên, thì khó để ta có thể truy cập và thay đổi được giá trị của biến name trong hàm f. Như vậy, biến name dường như đã private hơn.

    References

    [1] Andre Ye, Essential Python Concepts & Structures Any Serious Programmer Needs to Know, Explained

    [2] Fred Baptiste, Python Deep Dive, Part 1

    [3] Yang Zhou, 5 Levels of Understanding Closures in Python

    Authors

    [email protected]

  • Flow trong Coroutines Andorid

    Flow trong Coroutines Andorid

    Flow Trong Coroutines

    Trong coroutines , Flow là một kiểu có thể trả về nhiều giá trị một cách tuần tự trái ngược với việc dừng lại các hàm chỉ trả về 1 giá trị duy nhất để dễ hiểu thì khi bạn nhận giá trị trả về từ database là một list thì bạn sẽ phải tốn thời gian đợi database select hết giá trị rồi truyền vào list còn trong flow thì lấy được giá trị nào bạn có thể collect luôn được giá trị đó, rất tiện phải không nào , giả sử cái database của bạn có hàng chục triệu bản ghi xem , sài List là vã mồ hôi ngay =))

    Về cơ bản thì mọi anh em sẽ thấy Flow với Sequence khá giống nhau như kiểu cùng cha khác mẹ với ,thằng Flow thì nó xử lý bất đồng bộ còn Sequence là xử lý đồng bộ. Nếu bạn không hiểu thì cứ hiểu rằng Flow nó chạy song song với MainThread còn thằng Sequence sẽ Block MainThread lại để nó chạy xong đã .

    Code minh họa nhé

    Đây là sequence

    fun foo(): Sequence<Int> = sequence { 
        for (i in 1..3) {
            Thread.sleep(1000)
            yield(i) 
        }
    }
    
    fun main() = runBlocking {
        
        launch {
            println(Thread.currentThread().name)
            for (k in 1..3) {
                delay(1000)
                println("I'm blocked $k")
            }
        }
        val time = measureTimeMillis {
            foo().forEach { value -> println(value) }
        }
        println("$time s")
    }
    

    Output

    1
    2
    3
    3000 s
    main
    I'm blocked 1
    I'm blocked 2
    I'm blocked 3
    

    Anh em có thể thấy khi thằng Foo() chạy sẽ Lock MainThread lại không cho dòng For kia chạy mà đợi nó chạy xong thì mới unlock MainThread còn Flow thì sao nhìn ví dụ nhé

    fun foo(): Flow<Int> = flow {
        for (i in 1..3) {
            delay(1000)
            emit(i) 
        }
    }
    
    fun main() = runBlocking {
        launch {
            println(Thread.currentThread().name)
            for (k in 1..3) {
                delay(900)
                println("Running after Flow collect $k")
            }
        }
      
        val time = measureTimeMillis {
            foo().collect { value -> println(value) }
        }
        println("$time s")
    }
    

    Output

    main
    1
    Running after Flow collect 1
    2
    Running after Flow collect 2
    3
    Running after Flow collect 3
    3000 s
    

    Rõ ràng nó sẽ không Block MainThread lại mà cả hàm Foo và For đều chạy song song với nhau.

    1.Thêm Flow vào Andorid

    Vì nó nằm trong coroutines nên anh em import coroutines vào là xong, dễ mà vào build.gradle mà implementation thôi

    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
    

    2.Flow là các Cold Stream

    Trong RxJava, mỗi Observables đại diện cho một luồng dữ liệu, và phần thân của nó không được thực thi cho đến khi nó được đăng ký (subcribed) bởi một người đăng ký (subcriber) và sẽ nhận được dữ liệu khi nguồn phát ra dữ liệu.

    Flow hoạt động tương tự như vậy, nó cũng không nhận được dữ liệu cho đến khi collect dữ liệu.

    runBlocking {
        coroutinesFlow.foo()
        println("Delay 2s")
        delay(2000)
        coroutinesFlow.foo().collect {
           println(it)
        }
    }
    
    fun foo(): Flow<Int> = flow {
        println("Flow started")
        emit(1)
        delay(500)
        emit(2)
    }
    
    Delay 2s
    Flow started
    1
    2
    

    3.Flow cancellation

    Flow tuân thủ việc các nguyên tắc cancellation chung của coroutines. Việc collect của flow chỉ có thể bị hủy khi và chỉ khi flow đang bị suspend (chẳng hạn như gặp hàm delay) và ngược lại flow không thể bị hủy.

    Khi chúng ta sẽ sử dụng scope launch{} sẽ trả về 1 Job, từ Job này chúng ta có thể cancel scope đó, và nếu chúng ta đặt flow bên scope và khi cancel thì flow cũng bị cancel theo.

    fun cancelFlow() = runBlocking {
       val job = launch(Dispatchers.Default) {
           flow {
               (1..9).forEach {
                   delay(1000)
                   emit(it)
               }
           }.collect {
               println("value $it")
           }
       }
       delay(5000)
       job.cancel()
    }
    
    value 1
    value 2
    value 3
    value 4
    

    Kết quả chính xác sẽ là in ra từ value 1 -> value 9, nhưng do chúng ta delay 5s rồi cancel job nên flow cũng bị cancel từ đó luôn.

    Từ ví dụ này chúng ta biết được các scope lồng nhau thì khi scope cha bị cancel thì các scope con cũng bị cancel theo

    fun cancelScope() = runBlocking {
       val startTime = System.currentTimeMillis()
       val job = launch(Dispatchers.Default) {
           var nextPrintTime = startTime
           var i = 0
           while (i < 5) {
               if (System.currentTimeMillis() >= nextPrintTime) {
                   println("job: I'm sleeping ${i++} ...")
                   nextPrintTime += 1000L
               }
           }
       }
       delay(1300L)
       println("main: I'm tired of waiting!")
       job.cancel()
       println("main: Now I can quit.")
    }
    
    job: I'm sleeping 0 ...
    job: I'm sleeping 1 ...
    main: I'm tired of waiting!
    main: Now I can quit.
    job: I'm sleeping 2 ...
    job: I'm sleeping 3 ...
    job: I'm sleeping 4 ...
    

    Mặc dù đã gọi cancel để huỷ coroutine rồi nhưng vòng while kia vẫn chạy bất chấp, đó là bởi vì khi gọi cancel thì nó sẽ set lại 1 property là isActive từ true sang false, và mọi hàm support từ coroutine như delay, emit đều check xem isActive còn bằng true hay không? Nếu bằng false thì nó sẽ huỷ bỏ tiến trình đó luôn.

    Vậy sửa đoạn code trên ta chỉ cần thay điều kiện while (i < 5) sang while (isActive) là được.

    Qua ví dụ này chúng ta biết được để xem 1 coroutine bị cancel hay chưa, chỉ cần check property isActive

    4.Cách tạo Flow

    flowOf () – Nó được sử dụng để tạo luồng từ một tập giá trị nhất định.

    flowOf(4, 2, 5, 1, 7).onEach { delay(500) }
    

    asFlow () – Đây là một hàm mở rộng giúp chuyển đổi kiểu thành các luồng.

    (1..5).asFlow().onEach{ delay(500)}
    

    flow {} – Đây là một scope trình tạo để xây dựng các luồng tùy ý như các ví dụ trên.

    channelFlow {} (cold stream) – Cách này tạo ra luồng dữ liệu bằng cách sử dụng hàm send, giống như onNext trong RxJava Ví dụ:

    channelFlow {
        send(1)
    }.flowOn(Dispatchers.Default)
    

    Kết luận

    Anh em có thể thấy Flow rất mạnh trong việc xử lý bất đồng bộ cũng không kém gì Rxjava cả , các bạn có thể thấy Flow thường hay được sử dụng với LiveData trong Android Jetpack. Ở bài viết này còn rất nhiều thứ mình muốn đề cập đến như các toán tử , context , exception trong flow nhưng mình xin đề cập ở phần sau.

    Tài liệu tham khảo

    1. Kotlin Coroutines Flow

    2. Developer Kotlin Flow

    3. LiveData with Flow

  • Google Instant Apps

    Android Instant Apps – Trải nghiệm mới về Ứng dụng Android

    Mở đầu

    Tại sự kiện I/O 2016, Google đã công bố một khái niệm hoàn toàn mới về ứng dụng Android. Đó là Android Instant App. Đúng như tên gọi thì Instant Apps là những ứng dụng có thể được sử dụng ngay cả khi không cần cài đặt. Cũng giống như lúc đi chợ, khi "lựa hành" trên CH Play chúng ta đều muốn kiểm tra, "lật qua lật lại" "món hàng" mình định mua. Instant App hỗ trợ người dùng có thể "test nhanh" ứng dụng rồi mới quyết định có muốn tải về hay không, thay vì bị lừa bởi hình ảnh, video intro rồi tải về và xóa ngay lập tức vì thất vọng.

    Bài viết hôm nay chúng ta sẽ tìm hiểu về Android Instant Apps.

    Instant App là gì và sự khác biệt với Normal App như thế nào ?

    • Như phần giới thiệu, bạn cũng đoán được Instant App là gì. Hiểu một cách đơn giản, Instant App là một ứng dụng native giúp cho người dùng có thể ngay lập tức sử dụng ứng dụng của các nhà phát triển mà không cần cài đặt. Để xây dựng ứng dụng như vậy, các nhà phát triển cần tạo ra các module riêng và tích hợp với deep link, sau đó người dùng có thể nhấp vào URL để dùng thử ứng dụng.
    • Instant App vẫn được tải về như các app bình thường khác nhưng thay vì ở lại trên điện thoại của bạn, nó làm việc giống như bạn truy cập một website và sau đó thoát ra, nó chỉ lưu ứng dụng đó tạm thời và được xóa ngay khi không còn sử dụng.
    • Tiết kiệm thời gian và bộ nhớ sử dụng khi không chiếm dụng tài nguyên thiết bị cũng như mất thời gian download (những ngày nghỉ mà đứt cáp thì bạn sẽ càng hiểu rõ sự khác biệt này)
    • Một ví dụ ưu điểm nữa là khi bạn phát triển một ứng dụng như Instagram hoặc Youtube, muốn chia sẻ với bạn bè thì với Instant App thích hợp, chỉ cần đơn giản gửi 1 link là họ có thể dễ dàng và cực kỳ nhanh chóng xem được mà không cần phải cài đặt ứng dụng.

    Ưu điểm của Instant App (Góc nhìn của lập trình viên)

    1. Tăng khả năng khám phá Instant App đủ điều kiện để được giới thiệu trên trang chủ của Google Play Games, có khả năng tiếp cận với người dùng.
    2. Nhiều người chơi hơn Nếu người dùng không phải lo lắng về vấn đề cài đặt, họ có nhiều khả năng sẽ tham gia vào trò chơi của bạn.
    3. Khả năng kiếm tiền Các dịch vụ mua hàng trong ứng dụng và quảng cáo được hỗ trợ
    4. Trải nghiệm chất lượng cao Mọi thứ hoạt động ngay khi người dùng ấn vào "Instant Play"

    Làm thế nào để sử dụng Instant App ?

    Phần giới thiệu, ưu điểm có vẻ cực kỳ hoành tráng, thời gian ra mắt cũng được 5 năm nhưng nhiều bạn sẽ thắc mắc là lướt CH Play mấy năm trời có thấy cái Instant App nào đâu ?! Là do các bạn chưa kích hoạt nó mà thôi !

    • Vào Setting, tìm đến các cài đặt cho Google và tìm đến Google Play Instant. Tại đây bạn kích hoạt nó lên là có thể sử dụng Instant App của Google. (Tùy mỗi dòng điện thoại sẽ có những cách bố trí mục cài đặt khác nhau nên mình không thể chỉ rõ step-by-step được)

    Vậy làm thế nào để tạo ra một Instant App ?

    Cuối cùng thì phần mà các lập trình viên hóng nhất cũng đến. Cùng tìm hiểu cách tạo một Instant App đơn giản nào !

    I. Permission

    Vì sự nhỏ gọn, tiện lợi, Instant App không thể thực thi tất cả những tác vụ mà một App bình thường có thể làm. Cụ thể, nó chỉ có thể sử dụng các permissions sau:

    • ACCESS_COARSE_LOCATION
    • ACCESS_FINE_LOCATION
    • ACCESS_NETWORK_STATE
    • BILLINGDeprecated as of Play Billing Library 1.0.
    • CAMERA
    • INSTANT_APP_FOREGROUND_SERVICEOnly in Android 8.0 (API level 26) and higher.
    • INTERNET
    • READ_PHONE_NUMBERSOnly in Android 8.0 (API level 26) and higher.
    • RECORD_AUDIO
    • VIBRATE
    • WAKE_LOCK

    Thêm vào đó, các Instant App cũng không thể :

    • Sử dụng background services
    • Send notifications khi chạy trên background

    II. Kết nối với các ứng dụng đã được cài đặt

    Khi phát triển một Instant App, hãy nhớ rằng chỉ có thể tương tác với các ứng dụng đã cài đặt trên thiết bị nếu thỏa mãn một trong các điều kiện sau :

    • Một hoặc nhiều Activity trong các ứng dụng đó được cài đặt android:android:visibleToInstantApps=true – chỉ có sẵn cho Android 8.0 (API 26) trở lên.
    • Ứng dụng đã cài đặt chứa intent filter bao gồm CATEGORY_BROWSABLE
    • Instant App đang gửi một Intent bằng ACTION_SEND, ACTION_SENDTO hoặc ACTION_SEND_MULTIPLE

    III. Cấu hình Project

    1. Thêm khai báo sau vào app module build.gradle :

      implementation("com.google.android.gms:play-services-instantapps:17.0.0")
      
    2. Update targetSandboxVersion :

      <manifest
          xmlns:android="http://schemas.android.com/apk/res/android"
          ...
          android:targetSandboxVersion="2" ...>
      
    3. Khai báo các module hỗ trợ Instant App

      • View > Tool Windows > Project
      • Ấn chuột phải vào module, chọn Refactor > Enable Instant Apps Support
      • Chọn module tại dropdown menu
      • OK Android Studio thêm khai báo sau vào manifest của module:
      <manifest ... xmlns:dist="http://schemas.android.com/apk/distribution">
          <dist:module dist:instant="true" />
          ...
      </manifest>
      
    4. Code bất cứ thứ gì có thể vào module bạn đã chọn

    5. Deploy Instant App

      • Nếu bạn đã cài đặt ứng dụng (với bất kỳ phiên bản nào) trên thiết bị, uninstall nó đi
      • Run > Run/Debug Configurations, kích hoạt Deploy as instant app
      • Run > Run hoặc ấn biểu tượng Run trên toolbar để chạy Instant App.

    Nếu bước 4 của bạn được "thuận buồm xuôi gió" một Instant App sẽ được hiển thị lên thiết bị của bạn. Hãy thử back ra home screen và tìm một vòng xem có app nào được cài đặt không nhé ! – Dĩ nhiên là không rồi.

    Kết thúc

    Trong tương lai gần, có thể nói Instant App là một bước tiến lớn đối với trải nghiệm người dùng. Khi mà tốc độ các kết nối ngày càng nhanh chóng và lưu trữ đám mây trở nên phổ biến; thì việc sử dụng một ứng dụng ngay lập tức và không cần cài đặt là một điều thực sự tuyệt vời.

    Trên đây là một vài giới thiệu tổng quan nhất về Instant App, cũng như tầm phát triển và ý nghĩa mà nó mang lại. Cảm ơn các bạn đã giành thời gian theo dõi.

    Bạn có thể tìm hiểu sâu hơn về Instant App, xây dựng một ứng dụng tại: Android Developer – Google Play Instant

  • Android – Room Persistence Library

    Android – Room Persistence Library

    1. Room database là gì?

    Room là một Persistence Library được google giới thiệu trong sự kiện google I/O mới đây, nó là một abstract layer cung cấp cách thức truy câp thao tác với dữ liệu trong cơ sở dữ liệu SQLite. Các thành phần: Database, DAO (Data Access Object) và Entity.

    Các ứng dụng sử dụng một lượng lớn dữ liệu có cấu trúc có thể hưởng lợi lớn từ việc lưu lại dữ liệu trên local thông qua Room Database. Trường hợp thường gặp nhất là chỉ cache những dữ liệu có liên quan. Nếu làm vậy thì khi thiết bị không có kết nối internet thì user vẫn có thể truy cập data đấy khi đang offline. Mọi dữ liệu được phát sinh hay thay đổi do user sau đó sẽ được đồng bộ với server khi họ online trở lại.

    Room Database đơn giản hóa việc mã hóa và giảm thiểu các hoạt động liên quan đến cơ sở dữ liệu. Nếu các bạn đã chán ngán với việc phải khai báo các câu lệnh rất dài mới có thể xây dựng được database thì hãy sử dụng Room ngay nhé!

    2. Đặc điểm của Room database

    • Trong trường hợp SQLite, Không có xác minh thời gian biên dịch của các truy vấn SQLite thô. Nhưng trong Room có ​​xác thực SQL tại thời điểm biên dịch.
    • Khi lược đồ của bạn thay đổi, bạn cần cập nhật các truy vấn SQL bị ảnh hưởng theo cách thủ công. Room giải quyết vấn đề này.
    • Bạn cần sử dụng nhiều mã soạn sẵn để chuyển đổi giữa các truy vấn SQL và các Data objects. Tuy nhiên, Room ánh xạ các đối tượng cơ sở dữ liệu của chúng tôi tới Data objects mà không cần mã soạn sẵn.
    • Room được xây dựng để hoạt động với LiveData để quan sát dữ liệu, trong khi SQLite thì không.

    3. Cách import Room

    Mở build.gradle (app) và thêm dòng lệnh sau:

    dependencies {
        def room_version = "2.4.0"
    
        implementation "androidx.room:room-runtime:$room_version"
        annotationProcessor "androidx.room:room-compiler:$room_version"
    
        // optional - RxJava2 support for Room
        implementation "androidx.room:room-rxjava2:$room_version"
    
        // optional - RxJava3 support for Room
        implementation "androidx.room:room-rxjava3:$room_version"
    
        // optional - Guava support for Room, including Optional and ListenableFuture
        implementation "androidx.room:room-guava:$room_version"
    
        // optional - Test helpers
        testImplementation "androidx.room:room-testing:$room_version"
    
        // optional - Paging 3 Integration
        implementation "androidx.room:room-paging:2.4.0"
    }

    4. Các thành phần chính trong Room

    Có 3 thành phần chính trong Room:

    • Database

    Có thể dùng componenet này để tạo database holder. Annotation sẽ cung cấp danh sách các thực thể và nội dung class sẽ định nghĩa danh sách các DAO (đối tượng truy cập CSDL) của CSDL. Nó cũng là điểm truy cập chính cho các kết nối phía dưới.

    @Database(
        entities = [
            School::class,
            Student::class
        ],
        version = 2,
        exportSchema = false
    )
    abstract class SchoolDataBase : RoomDatabase() {
    
        abstract fun schoolDAO(): ISchoolDAO
    
        abstract fun studentDAO(): IStudentDAO
    
        companion object {
            @Volatile
            private var INSTANCE: SchoolDataBase? = null
    
            fun getInstance(context: Context): SchoolDataBase = INSTANCE ?: synchronized(this) {
                return Room.databaseBuilder(
                    context.applicationContext,
                    SchoolDataBase::class.java,
                    Constant.SCHOOL_DATABASE
                ).build().also {
                    INSTANCE = it
                }
            }
        }
    }
    • Entity

    Component này đại diện cho một class chứa một row của database. Với mỗi một entity thì một database table sẽ được tạo để giữ các items tương ứng. Nên tham chiếu lớp enity thông qua mảng entities trong class Database.

    Mỗi Object phải xác định ít nhất 1 trường làm khóa chính. Ngay cả khi chỉ có 1 trường, bạn vẫn cần chú thích trường này bằng anotation @PrimaryKey. Ngoài ra, nếu bạn muốn Room gán ID tự động cho các thực thể, bạn có thể đặt thuộc tính autoGenerate của @PrimaryKey.(Trường hợp thuộc tính là int, long).

    @Entity(tableName = SCHOOL_TABLE)
    data class School(
        @PrimaryKey(autoGenerate = true)
        @ColumnInfo(name = SCHOOL_ID)
        val schoolId: Int = 0,
        @ColumnInfo(name = SCHOOL_NAME)
        val schoolName: String,
        @ColumnInfo(name = SCHOOL_ADDRESS)
        val schoolAddress: String
    )

    Trường hợp các bạn muốn đánh index cho một số trường trong database để tăng tốc độ truy vấn các bạn có thể sử dụng như sau:

    @Entity(indices = {@Index(value = {"first_name", "last_name"}})
    

    Định nghĩa foreignKeys. Ví dụ bạn có đối tượng khác là Student và bạn có thể định nghĩa relationship tới đối tượng School thông qua @ForeignKey annotation như sau:

    @Entity(
        tableName = STUDENT_TABLE,
        foreignKeys = [ForeignKey(
            entity = School::class,
            parentColumns = [SCHOOL_ID],
            childColumns = [SCHOOL_ID],
            onDelete = ForeignKey.CASCADE
    
        )]
    )
    data class Student(
        @PrimaryKey(autoGenerate = true)
        @ColumnInfo(name = STUDENT_ID)
        val studentId: Int = 0,
        @ColumnInfo(name = SCHOOL_ID)
        val schoolId: Int,
        @ColumnInfo(name = STUDENT_NAME)
        val studentName: String,
        @ColumnInfo(name = STUDENT_GRADE)
        val studentGrade: Float
    )
    • DAO (Data Access Object)

    Đây là component đại diện cho lớp hoặc interface như một đối tượng truy cập dữ liệu (DAO). DAO là thành phần chính của Room là chịu trách nhiệm trong việc định nghĩa các phương thức truy cập CSDL.

    @Dao
    interface ISchoolDAO {
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        suspend fun insertSchool(school: School)
    
        @Delete
        suspend fun deleteSchool(school: School)
    
    
        @Update
        suspend fun updateSchool(school: School)    
    
        @Transaction
        @Query("SELECT * FROM $SCHOOL_TABLE")
        fun getAllSchool(): Flow<List<SchoolAndStudent>>
    }

    5. Kết luận

    Trên đây là phần giới thiệu cơ bản về Room database của mình.

    Cám ơn các bạn đã đọc bài viết của mình.

    Tham khảo: https://developer.android.com/training/data-storage/room#groovy

  • Neighborhood-Based Collaborative Filtering – Recommendation Systems – Phần 3

    Neighborhood-Based Collaborative Filtering – Recommendation Systems – Phần 3

    Neighborhood-Based Collaborative Filtering – Recommendation Systems – Phần 3

    Xin chào mọi người, ở bài viết trước thì mình đã giới thiệu qua về phương pháp User-based và item-based trong CF , ở bài viết lần này mình sẽ triển khai code Python để anh em có cái nhìn rõ hơn nhé.

    Nội dung trong phần này

    Giới thiệu

    Dữ liệu kiểm thử lần này mình sẽ lấy bộ dữ liệu MovieLens 100k được công bố năm 1998 bởi GroupLens. Bộ cơ sở dữ liệu này bao gồm 100,000 (100k) ratings từ 943 users cho 1682 bộ phim. Các bạn cũng có thể tìm thấy các bộ cơ sở dữ liệu tương tự với khoảng 1M, 10M, 20M ratings. Trong bài viết này, tôi sử dụng bộ cơ sở dữ liệu nhỏ nhất này nhằm mục đích minh hoạ.

    Sau khi download và giải nén, chúng ta sẽ thu được rất nhiều các file nhỏ, chúng ta chỉ cần quan tâm các file sau:

    • u.data : Chứa toàn bộ các ratings của 943 users cho 1682 movies. Mỗi user rate ít nhất 20 movies. Thông tin về thời gian rate cũng được cho nhưng chúng ta không sử dụng trong bài viết này.
    • ua.base, ua.test, ub.base, ub.test: là hai cách chia toàn bộ dữ liệu ra thành hai tập con, một cho training, một cho test. Chúng ta sẽ thực hành trên ua.base và ua.test. Bạn đọc có thể thử với cách chia dữ liệu còn lại.
    • u.user: Chứa thông tin về users, bao gồm: id, tuổi, giới tính, nghề nghiệp, zipcode (vùng miền), vì những thông tin này cũng có thể ảnh hưởng tới sở thích của các users. Tuy nhiên, trong bài viết này, chúng ta sẽ không sử dụng các thông tin này, trừ thông tin về id để xác định các user khác nhau.

    Phương pháp đánh giá

    Để đánh giá mô hình thì chúng ta sẽ sử dụng hai giá trị đó là :

    • Mean Absolute Error (MAE) :độ đo để đánh giá các mô hình hồi quy. MAE được định nghĩa là trung bình tổng trị tuyệt đối sai số giữa đầu ra dự đoán và kết quả thực . Công thức là :

    MAE

    • Root Mean Squared Error (RMSE):độ đo để đánh giá các mô hình hồi quy. RMSE được định nghĩa là căn bậc 2 trung bình tổng bình phương sai số giữa đầu ra dự đoán và kết quả thực:

    RMSE

    Code python

    import pandas as pd 
    import numpy as np
    from sklearn.metrics.pairwise import cosine_similarity
    from scipy import sparse 
    from numpy.linalg import matrix_power
    
    class CF(object):
       
       #Khai báo cho dữ liệu đầu vào là ma trận tiện ích , K item , hàm tính similarity , Tag User-based hoặc Item-Based 
       def __init__(self, Y_data, k, dist_func = cosine_similarity, uuCF = 1):
           # quy ước user-based (1) và  item-based (0) CF
           self.uuCF = uuCF 
           self.Y_data = Y_data if uuCF else Y_data[:, [1, 0, 2]]
           self.k = k
           self.dist_func = dist_func
           self.Ybar_data = None
           # Số lượng User và item . Vì id của user hoặc item được đếm từ 0 nên + thêm 1
           self.n_users = int(np.max(self.Y_data[:, 0])) + 1 
           self.n_items = int(np.max(self.Y_data[:, 1])) + 1
       
       
    
      
       def normalize_Y(self):
           # Tất cả các user và item 
           users = self.Y_data[:, 0]
           items = self.Y_data[:,1]
           
           # Tạo một ma trận để biến đổi giữ lại ma trận ban đầu để so sánh khi tính RMSE
           self.Ybar_data = self.Y_data.copy()
           self.mu = np.zeros((self.n_users,))
           for n in range(self.n_users):
               # Lấy id của User đã ratings
               # Convert lại thành kiểu Integer
               ids = np.where(users == n)[0].astype(np.int32)
               # Lấy id item được User có id ở trên rated
               item_ids = self.Y_data[ids, 1] 
               # Lấy giá trị ratings 
               ratings = self.Y_data[ids, 2]
               # Tính giá trị trung bình
               m = np.mean(ratings) 
               if np.isnan(m):
                   m = 0 # nếu ratings không có giá trị set = 0
               self.mu[n] = m
               # chuẩn hóa lại ma trận 
               self.Ybar_data[ids, 2] = ratings - self.mu[n]
    
           
           # Để tránh không đủ bộ nhớ khi số lượng người dùng và item quá lớn thì chúng ta chỉ lưu vị trí của các ratings khác 0
           self.Ybar = sparse.coo_matrix((self.Ybar_data[:, 2],
               (self.Ybar_data[:, 1], self.Ybar_data[:, 0])), (self.n_items, self.n_users))
           self.Ybar = self.Ybar.tocsr()
           
       
    
       # Hàm tính toán độ tương đồng giữa các User hoặc Item , ở đây tôi dùng thư viện cosine_similarity
       def similarity(self):
           eps = 1e-6
           self.S = self.dist_func(self.Ybar.T, self.Ybar.T)
       
           
       # Chuẩn hóa lại data và tính toán ma trận Similarity sau khi Đã điền ratings của các item chưa được rate
       def refresh(self):
           
           self.normalize_Y()
           self.similarity() 
           
       def fit(self):
           self.refresh()
           
       
       
       # Chuẩn đoán rating của user u for item i 
       def __pred(self, u, i, normalized = 1):
           
           
           # Step 1: Tìm tất cả user đã rated cho item i
           ids = np.where(self.Y_data[:, 1] == i)[0].astype(np.int32)
           # Step 2: 
           users_rated_i = (self.Y_data[ids, 0]).astype(np.int32)
           # Step 3: Tìm sự tương đồng giữa User n với các User còn lại
           sim = self.S[u, users_rated_i]
           # Step 4: Tìm K User có sự tương đồng nhất
           a = np.argsort(sim)[-self.k:] 
           nearest_s = sim[a]
           
           r = self.Ybar[i, users_rated_i[a]]
           if normalized:
               # Thêm 1 giá trị nhỏ để tránh chia cho 0
               return (r*nearest_s)[0]/(np.abs(nearest_s).sum() + 1e-8)
    
           return (r*nearest_s)[0]/(np.abs(nearest_s).sum() + 1e-8) + self.mu[u]
       
       def pred(self, u, i, normalized = 1):
           
           # Dự đoán ratings user u cho item i
           if self.uuCF: return self.__pred(u, i, normalized)
           return self.__pred(i, u, normalized)
               
       
       def recommend(self, u):
           
           # Đề xuất item cho User u với ratings > 0 
           ids = np.where(self.Y_data[:, 0] == u)[0]
           items_rated_by_u = self.Y_data[ids, 1].tolist()              
           recommended_items = []
           for i in range(self.n_items):
               if i not in items_rated_by_u:
                   rating = self.__pred(u, i)
                   if rating > 0: 
                       recommended_items.append(i)
           
           return recommended_items 
       
       def recommend2(self, u):
          # Để xuất User cho item u với ratings > 0 
           ids = np.where(self.Y_data[:, 0] == u)[0]
           items_rated_by_u = self.Y_data[ids, 1].tolist()              
           recommended_items = []
       
           for i in range(self.n_items):
               if i not in items_rated_by_u:
                   rating = self.__pred(u, i)
                   if rating > 0: 
                       recommended_items.append(i)
           
           return recommended_items 
    
       def print_recommendation(self):
           
           # Print ra đề xuất
           print ('Recommendation: ')
           for u in range(self.n_users):
               recommended_items = self.recommend(u)
               if self.uuCF:
                   print ('Recommend item(s):', recommended_items , 'for user', u)
             
               else: 
                   print ('Recommend item', u , 'for user(s) : ', recommended_items)
               
    

    So sánh

    User-based

    rs = CF(rate_train, k = 30, uuCF = 0)
    rs.fit()
    
    n_tests = rate_test.shape[0]
    SE = 0 # squared error
    AE = 0
    for n in range(n_tests):
        pred = rs.pred(rate_test[n, 0], rate_test[n, 1], normalized = 0)
        SE += (pred - rate_test[n, 2])**2
        AE += (pred - rate_test[n,2])
    
    
    RMSE = np.sqrt(SE/n_tests)
    MAE = abs(AE/n_tests)
    print (' RMSE =', RMSE)
    print (' MAE =', MAE)
    

    Kết quả : RMSE = 0.9867912132705384 và MAE = 0.0582653956596381

    Có thể thấy MAE và RMSE rất nhỏ cho thấy hệ thống chạy khá ok

    Item-based

    rs = CF(rate_train, k = 30, uuCF = 1)
    rs.fit()
    
    n_tests = rate_test.shape[0]
    SE = 0 # squared error
    AE = 0
    for n in range(n_tests):
       pred = rs.pred(rate_test[n, 0], rate_test[n, 1], normalized = 0)
       SE += (pred - rate_test[n, 2])**2
       AE += (pred - rate_test[n,2])
       
    MAE = abs(AE/n_tests)
    RMSE = np.sqrt(SE/n_tests)
    print (' RMSE =', RMSE)
    print (' MAE =', MAE)
    

    Kết quả : RMSE = 0.9951981100882598 và MAE = 0.03062686975303437

    Kết quả cũng rất ấn tượng khi MAE còn thấp hơn cả phương pháp User-based nhưng về cơ bản không có sự cách biệt quá lớn, các bạn cũng có thể thay hệ số K lớn hơn để thấy rõ được sự khác biệt về kết quả . Thực ra kết quả này bị ảnh hướng rất nhiều từ file data , đôi khi dữ liệu về item lại cực lớn so với user thì phương pháp item based lại tỏ ra mạnh mẽ hơn và ngược lại . Thực tế thì hai cách này luôn đi song song với nhau để đưa ra tư vấn cho người dùng.

    Mình cũng xin kết thúc bài viết ở đây , ở bài tiếp theo mình sẽ đề cập đến phương kết hợp giữa cả user và item .

    Tài liệu mình hay tham khảo

    1. Khá hay và nhiều kiến thức của các pháp sư US-UK AWS Machine Learning Blog

    2. Kho Dataset cho anh em GroupLens

    3. Mình cũng hay tham khảo ở đây TensorFlowBlog