Blog

  • Triển khai CI/CD cho iOS – SonarQube & Blackduck

    Overview

    Tiếp tục với series CI/CD cho iOS, hôm nay chúng ta sẽ triển khai CI với hai nền tảng kiểm tra source code rất nổi tiếng là SonarQube và Blackduck.

    Triển khai CI với Blackduck

    Khác với SonarQube, Blackduck không đánh giá chất lượng mà giúp chúng ta quản trị open source code và source code từ các thư viện được thêm vào. Giúp chúng ta đánh giá và quản lý được các rủi ro về bản quyền, bảo mật khi sử dụng source code có sẵn trên mạng cũng như lưu hành trong cộng đồng.

    Do Blackduck không có Server public như Sonar, nên mình sẽ giả định chúng ta có một Server Blackduck được đặt tại địa chỉ sau:

    https://blackduck.techover.io

    Sau khi truy cập vào, chúng ta sẽ thấy danh sách các project đã có ở Dashboard. Ở đây mình đã tạo sẵn một Project W95.CICD, nếu muốn tạo mới chúng ta sẽ ấn nút Create Project ở góc trên bên phải.

    Dashboard của Blackduck

    Cũng giống như Sonar, để có thể đồng bộ các dữ liệu Scan chúng ta cần có một Token. Để tạo Blackduck Token, chúng ta sẽ vào màn hình quản lý Access Tokens như sau:

    Ấn vào Profile ở góc trên bên phải, chọn My Access Tokens

    Sau khi chuyển đến màn hình quản lý Access Token, chúng ta ấn Create New Access Token và điền thông tin.

    Màn hình quản lý Access Tokens
    Nhập thông tin và ấn Create

    Sau khi có token, chúng ta sẽ lưu lại để sử dụng sau này. Lưu ý, token sẽ không hiển thị lại lần thứ 2 nên hãy copy và lưu lại ngay và luôn nhé. Ví dụ mình sẽ lưu lại token lại dưới đây:

    YWJmYzc5MDMS05N2VjLWFkNGE4ZMS05N2VjLWFkNGE4Z--=/

    Đối với Blackduck, chúng ta cũng sử dụng gần giống như Sonar nhưng thay vì CLI, chúng ta sẽ sử dụng Java Archive, vì vậy hãy đảm bảo bạn đã cài Java trong thiết bị Runner nhé.

    Tải về phiên bản .jar mới nhất của blackduck ở đây

    Sau khi tải xong, chúng ta sẽ giải nén và lưu và một thư mục trong thiết bị Runner, ở đây mình sẽ lưu ở địa chỉ sau:

    /Users/jena/CICD/synopsys-detect/synopsys-detect-6.4.1.jar

    Rồi xong, tới công chuyện luôn!!

    Tiếp đó chúng ta chỉ cần cấu hình file .gitlab-ci.yml như sau:

    stages:
      - Lint
    blackduck-detect:
      stage: Lint
      only:
          - cicd
      script:
        - java -jar /Users/jena/CICD/synopsys-detect/synopsys-detect-6.4.1.jar \
          --blackduck.url=https://blackduck.techover.io \
          --blackduck.api.token=YWJmYzc5MDMS05N2VjLWFkNGE4ZMS05N2VjLWFkNGE4Z--=/ \
          --detect.project.name=W95.CICD
      tags:
        - w95

    Vậy là xong, mỗi khi có commit/merge lên nhánh cicd (hãy thay bằng master/main/develop) thì hệ thống sẽ tự động scan source code và gửi kết quả lên Server.

    Quên mất, còn một bước cuối cùng nữa là bạn có thể che đi các thông tin nhạy cảm trong file cấu hình .gitlab-ci.yml, đề phòng trong trường hợp file cấu hình bị rò rỉ, các thông tin về Server Blackduck cũng như Token cũng sẽ không bị ảnh hưởng. Để làm việc này chúng ta sẽ cấu hình một số thông tin sau vào biến môi trường của Gitlab-CI

    BLACKDUCK_SERVER = https://blackduck.techover.io
    BLACKDUCK_TOKEN = YWJmYzc5MDMS05N2VjLWFkNGE4ZMS05N2VjLWFkNGE4Z--=/

    Và kết quả, file .gitlab-ci.yml sẽ trông như sau:

    stages:
      - Lint
    blackduck-detect:
      stage: Lint
      only:
          - cicd
      script:
        - java -jar /Users/jena/CICD/synopsys-detect/synopsys-detect-6.4.1.jar \
          --blackduck.url=${BLACKDUCK_SERVER} \
          --blackduck.api.token=${BLACKDUCK_TOKEN} \
          --detect.project.name=W95.CICD
      tags:
        - w95

    Rồi xong, tới công chuyện luôn!! Như vậy là chúng ta đã hoàn thành cấu hình CI với Blackduck để scan các lỗi hổng bảo mật, bản quyền. Mặc dù liên quan đến CI còn rất nhiều section như Coverity, Build & Compile nhưng mình xin phép tạm dừng hạng mục CI và chuyển sang CD. Rất mong được các bạn ủng hộ.

  • Triển khai CI/CD cho iOS – SonarQube & Blackduck

    Triển khai CI/CD cho iOS – SonarQube & Blackduck

    Overview

    Tiếp tục với series CI/CD cho iOS, hôm nay chúng ta sẽ triển khai CI với hai nền tảng kiểm tra source code rất nổi tiếng là SonarQube và Blackduck.

    Triển khai CI với SonarQube

    Đôi chút về SonarQube, đây là một nền tảng mã nguồn mở sử dụng để kiểm tra chất lượng của source code, đánh giá các lỗi ở nhiều mức độ và tiêu chí khác nhau. Mục đích cuối cùng là để thống kê và cải thiện chất lượng của source code theo mọi mặt cũng như giúp lập trình viên đánh giá chất lượng của chính mình. Vì vậy, việc sử dụng SonarQube để hỗ trợ quá trình phát triển, nâng cao chất lượng source code luôn được các doanh nghiệp lớn áp dụng.

    Chúng ta sẽ thực hiện CI với SonarQube server ở địa chỉ sau: https://sonarcloud.io, bạn có thể sử dụng tài khoản GitLab của mình để đăng nhập và tạo project ở đây. Sau một vài bước đăng nhập, tạo organization và chọn plan thì chúng ta sẽ đến với step đầu tiên.

    Đối với một số trường hợp có Server riêng để host SonarQube, các bạn hãy truy cập host và sử dụng tài khoản/mật khẩu được cung cấp bởi admin.

    Cấu hình SonarQube

    Việc đầu tiên, chúng ta sẽ cần tạo một project, ở đây mình sẽ tạo một project như hình sau:

    Điền Project Key và Display name sau đó ấn Set Up

    Trong một số trường hợp, bước config tạo Project sẽ do admin tạo, các bạn chỉ cần đăng nhập là xem được các project mình được phân quyền.

    Sau khi ấn Set Up, chúng ta sẽ được suggest 3 lựa chọn, ở đây mình sẽ chọn Manually để có thể sử dụng ở nhiều nền tảng khác nhau (Không chỉ riêng GitLab-CI).

    Lựa chọn Manually

    Tiếp đó chúng ta sẽ chọn các lựa chọn như hình sau:

    Chọn Other(…) -> macOS

    Tiếp đó chúng ta ấn Download để tải CLI của SonarQube về, giải nén và lưu vào một thư mục trong thiết bị runner. Giả sử mình sẽ lưu ở địa chỉ sau:

    /Users/jena/Projects/sonar-scanner/bin

    Mình sẽ thêm thư mục bin vào trong PATH của macOS bằng câu lệnh sau

    export PATH=$PATH:/Users/jena/Projects/sonar-scanner/bin

    Sau đó, bạn có thể chạy lệnh sau để kiểm tra xem cli đã được nhận vào PATH chưa. Sẽ có một số lỗi yêu cầu cấp quyền để chạy CLI, bạn hãy vào System Preference -> Security & Privacy -> Tab General -> Allow Anyway tất cả

    sonar-scanner -v
    Sau khi cli được add vào PATH, bạn có thể chạy bằng lệnh sonar-scanner

    Tiếp đó, chúng ta sẽ vào GitLab và cài đặt biến môi trường SONAR_TOKEN như hình sau

    Tiếp đó, chúng ta sẽ cấu hình CI ở file .gitlab-ci.yml như sau, phần script sẽ được gen cùng với SONAR_TOKEN, các bạn chỉ cần copy và paster vào là được:

    stages:
      - Lint
    sonar-scanner:
      stage: Lint
      only:
          - cicd
      script:
        - sonar-scanner -Dsonar.organization=w95 -Dsonar.projectKey=CICD.iOS -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.branch=master
      tags:
        - w95

    Sau khi commit file .gitlab-ci.yml lên branch cicd, chúng ta sẽ có kết quả như sau:

    Job Sonar Scanner chạy thành công với log như trên
    Màn hình thống kê trên SonarCloud.io cũng sẽ hiển thị các thông số của source code

    Theo như ảnh trên, Quality Gate đang đánh giá Passed tức là source code đạt chất lượng, nhưng thật ra mình scan Starter Project của iOS nên mới không có lỗi, còn code của mình thì lắm lỗi lắm :p

    Như vậy là chúng ta đã hoàn thành bước cấu hình CI sử dụng SonarQube cho một project iOS. Mỗi khi có commit, merge hay sự thay đổi trên branch cicd (bạn sẽ đổi thành master/develop/main) thì hệ thống sẽ tự động chạy CI và đẩy thống kê lên Sonar server. Chúng ta chỉ cần lên đó, tracking các thông số và sửa các lỗi bị cảnh báo là được.

    Do bài viết hơi dài, nên mình sẽ để phần Blackduck sang bài viết sau. Cảm ơn các bạn đã đọc!

    Authors

    LinhNB1

  • Triển khai CI/CD cho iOS – SwiftLint

    Triển khai CI/CD cho iOS – SwiftLint

    Hưởng ứng theo tinh thần của Editor team, mình đóng góp Series này để hưởng ứng Technopedia, không nhằm mục đích dự thi. Mong rằng các kinh nghiệm của mình sẽ giúp ích được cho cộng đồng trong lĩnh vực liên quan.

    Sam

    Để triển khai CI/CD cho một sản phẩm iOS có rất nhiều lựa chọn, chúng ta có thể sử dụng GitLab-CI, Xcode Server, Fastlane, Jenkins, Microsoft App Center, Circle CI, …
    Ở phạm vi bài viết này, chúng ta sẽ đề cập đến một nền tảng được tích hợp với GitLab: GitLab-CI

    Việc triển khai CI/CD cho một dự án iOS Swift bao gồm một số phần sau:

    1. Triển khai CI với SwiftLint
    2. Triển khai CI với SonarQube, Blackduck
    3. Triển khai CD đơn giản với Gitlab-CI Artifact
    4. Triển khai CD In-house với DeployGate.
    5. Triển khai CD OTA Inhouse trên Website với AWS S3 (Static Website Hosting)
    6. Triển khai CD với Appstore Connect

    Cài đặt và khởi tạo Runner

    Đầu tiên, các chúng ta cần cài đặt và khởi tạo Runner cho Gitlab Repo. Mình có viết một bải hướng dẫn ở đây
    Chú ý:
    Sau khi đăng ký runner, nhưng các job vẫn chạy trên container mặc định của GitLab-CI thì hãy chạy các lệnh sau:

    gitlab-runner install
    gitlab-runner start
    gitlab-runner status

    1. Triển khai CI với SwiftLint

    Đầu tiên, chúng ta sẽ đi đến việc cấu hình CI sử dụng SwiftLint để phân tích chất lượng source code.

    Tốt nhất chúng ta sẽ cài đặt và sử dụng Swiftlint tách biệt với source code của dự án như sau

    brew install swiftlint

    Sau khi cài đặt Swiftlint, ta có thể test bằng cách di chuyển vào thư mục root của source code và chạy lệnh

    swiftlint

    Kết quả lint source code sẽ hiển thị như sau, ví dụ ở đây ta có 16 lỗi.

    Sau đó, chúng ta cấu hình file .gitlab-ci.yml để chạy lint trên branch cicd như sau:

    stages:
      - Lint
    lint-source:
      stage: Lint
      only:
          - cicd
      script:
        - swiftlint

    Kết quả khi commit code lên branch cicd, chúng ta sẽ có kết quả log như sau:

    Mặc định Swiftlint job sẽ trả về kết quả thành công exit 0


    Ở đây chúng ta thấy, hệ thống đã phát hiện được 16 lỗi ở 3 files code. Tuy nhiên, job vẫn success và các merge request vẫn được phép tiếp tục vì Swiftlint vẫn trả về success thay vì error. Đây là một rủi ro, và để ép chặt các thành viên phải fix hết các lỗi Swiftlint trước khi được merge code, ta sẽ thêm tham số vào script như sau:

    script:
      - swiftlint --strict

    Kết quả thu được, hệ thống scan code và trả về lỗi nếu swiftlint chưa được fix hết.

    Swiftlint job sẽ trả về lỗi exit 1 khi sử dụng tham số –strict


    Trong trường hợp chúng ta muốn “dễ dãi”, cho phép merge code trong trường hợp các lỗi swiftlint vẫn còn hoặc muốn job trả về thành công để tiếp tục các job tiếp theo, ta sẽ cấu hình thêm một chút như sau:

    stages:
      - Lint
    lint-source:
      stage: Lint
      allow_failure: true
      only:
        - cicd
      script:
        - swiftlint --strict
    Các job fail khi cấu hình allow_failure = true


    Khi ấy, các job phía sau vẫn sẽ được thực hiện, merge request vẫn sẽ được approve nhưng với thông báo Warning đáng chú ý hơn.

    Như vậy là đã hoàn thành công đoạn triển khai CI với Swiftlint, ở bài viết tiếp theo, mình sẽ hướng dẫn các bạn triển khai CI với SonarQube, Blackduck

    Authors

    LinhNB1

  • [Spring] Sử dụng Spring ResponseStatusException

    [Spring] Sử dụng Spring ResponseStatusException

    Sử dụng Spring ResponseStatusException

    Giới thiệu

    Một ứng dụng RESTful, bằng cách trả về các HTTP status code trong HTTP response nó có thể thông báo về sự thành công hay thất bại của một HTTP request. Ví dụ như nếu người dùng request lên một id không hề tồn tại, các HTTP status code có thể giúp xác định được các vấn đề có thể xảy ra khi xử lí request.

    Trong Spring chúng ta cũng có rất nhiều cách để đặt HTTP status code cho một HTTP response. Tuy nhiên trong bài viết này mình sẽ giới thiệu về một class mới được giới thiệu trong Spring 5 đó chính là ResponseStatusException sẽ hỗ trợ cho ta việc áp dụng HTTP status code.

    @ResponseStatus

    Trước khi tìm hiểu về ResponseStatusException , ta sẽ tìm hiểu qua về @ResponseStatus annotation. Annotation này được giới thiệu trong Spring 3 để giải quyết vấn đề áp dụng HTTP status code cho HTTP response.

    Với annotaion này chúng ta sẽ sử dụng để định nghĩa status code và reason cho HTTP response:

    import org.springframework.http.HttpStatus;
    import org.springframework.web.bind.annotation.ResponseStatus;
    
    @ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Image not found")
    public class ImageNotFoundException extends Exception{
        //...
    }
    

    Ở trong ví dụ này, nếu Exception được đưa ra trong một xử lí HTTP request, thì trong response sẽ bao gồm HTTP status code được chỉ định sẽ là 404.

    Tuy nhiên với phương pháp sử dụng @ResponseStatus ta có một nhược điểm là nó sẽ tạo ra mối quan hệ phụ thuộc chặt chẽ vói Exception. Và như trường hợp ta xét ở trên thì tất cả các Exception có kiểu ImageNotFoundException khi được đưa ra thì đều cho ta một response với thông báo lỗi và status code là như nhau trong mọi trường hợp.

    ResponseStatusException

    ResponseStatusException được tạo ra nhằm thay thế cho @ResponseStatus và là một base class cho các exception được sử dụng đề apply các HTTP status cho response. Và đây cũng là một RuntimeException.

    Với ResponseStatusException class sẽ cung cấp cho ta 3 contructor:

    Với các đối số truyền vào cho contructor method:

    • status – HTTP status được set cho HTTP response
    • reason – message được hiển thị để giải thích cho exception
    • cause – là một java.lang.Throwable nguyên nhân của ResponseStatusException

    Những điểm mạnh cảu việc dùng ResponseStatusException class:

    • Thứ nhất, việc khai báo và sử dụng dễ dàng
    • Thứ hai, các exception cùng loại ta có thể xử lí riêng biệt và có thể linh hoạt các stutas code khác nhau có thể được set trong response, giảm sự phụ thuộc vào nhau.
    • Thứ ba, ta có thể tránh được việc phải tạo các class exception không cần thiết.
    • Và cuối cùng, class cấp cho ta nhiều quyền hơn trong việc xử lí exception, vì các exception được tạo theo chương trình nên được kiểm soát tốt hơn.

    Ví dụ

    Bây giờ, hãy xem một ví dụ về cách sử dụng ResponseStatusException trong thực tế:

    Ta có 1 class ToDoController khái báo “/todo” mapping truyền vào tham số id để lấy ra Todo object tương ứng

    @RestController
    public class ToDoController {
    
        @Autowired
        private ToDoService toDoService;
    
        @GetMapping(value = "/todo")
        public ToDo getTodo(@RequestParam(value = "id", required = false) Long id) {
            try {
                return toDoService.getTodo(id);
            } catch (TodoNotFoundException e) {
                throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Todo not found", e);
            }
        }
    
    }
    

    Với một exception được khai báo

    public class TodoNotFoundException extends Exception{
    
        public TodoNotFoundException(String errorMessage){
            super(errorMessage);
        }
    }
    

    Spring sẽ cung cấp cho ta một “/error” mapping sẽ trả về response dưới định dạng JSON với HTTP status. Trong ví dụ này khi truyền vào id không hề tồn tại chương trình sẽ trả về ResponseStatusException với response chứa status code tương ứng.

    Đây sẽ là response trong trường hợp có exception (ta sẽ dùng Postman để gửi request):

    Để xem được message về lỗi trong response ta sẽ thêm thuộc tính server.error.include-message=always. Khi đó response trả về sẽ có nội dung:

    {
        "timestamp": "2021-08-03T01:35:20.878+00:00",
        "status": 404,
        "error": "Not Found",
        "message": "Todo not found",
        "path": "/todo"
    }
    

    Ngoài ra với lợi ích là tính linh hoạt khi có thể gán các status code khác nhau cho cùng một exception ta có thể thứ với một ví dụ khác:

    @GetMapping(value = "/todo")
        public ToDo getTodo(@RequestParam(value = "id", required = false) Long id) {
            try {
                return toDoService.getTodo(id);
            } catch (TodoNotFoundException e) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Provide correct Todo Id", e);
            }
        }
    

    Và response trả về sẽ là:

    {
        "timestamp": "2021-08-03T01:57:41.502+00:00",
        "status": 400,
        "error": "Bad Request",
        "message": "Provide correct Todo Id",
        "path": "/todo"
    }
    

    Lời kết

    Như vậy trong bài viết này, chúng ta đã cùng tìm hiểu về ResponseStatusException – một cách tốt hơn để tạo một HTTP status code trong HTTP response so với annotation @ResponseStatus. Tuy nhiên với việc nên thận trọng với việc sử dụng vì nếu không có phương pháp xử lí exception thống nhất thì việc thực thi một số quy ước trên toàn ứng dụng sẽ khó khăn và có thể xảy ra code bị trùng lặp.

    Cảm ơn các bạn đã đọc bài viết!

    Tài liệu tham khảo: https://www.baeldung.com/spring-response-status-exception

  • [DesignPattern] Simple Factory Pattern

    [DesignPattern] Simple Factory Pattern

    Background

    Hầu hết anh em developer đều đã nghe qua về Design Pattern

    Tuy nhiên mình thấy còn nhiều người (gồm cả mình) đều không có kinh nghiêm áp dụng nó.

    Nên mình lập topic về design pattern để mọi người có thể chia sẻ kinh nghiệm và cách áp dụng nó trong các bài toán cụ thể.

    Vậy đầu tiên chúng ta phải hiểu design pattern là gì?

    • Theo mình hiểu design pattern đơn giản là các giải pháp mẫu tối ưu cho từng tình huống cụ thể trong lập trình OOP.

    Taị sao lại sử dụng design pattern?

    Một thực tế là mình nghe rất nhiều câu như refactor đi (dm code như shit, fucking coding …),đập hết đi xây lại.

    Tại sao chúng ta lại muốn như thế?

    Vì hầu hết các source code ban đầu đều rất khó maintain và mở rộng, nên khi có một yêu cầu mới hay một bug,

    anh em lại cặm cụi sửa code, sửa bug này lại sinh ra bug khác nên effort để giải quyết một vấn đề rất tốn kém.

    Do đó nếu mình không chỉ "code để chạy được" mà suy nghĩ, áp dụng các design pattern trươc khi viết ra

    sẽ rút ngắn phần lớn thời gian development và maintain.

    Sau đây mình xin trình bầy một số design pattern mà mình đã đọc qua.

    Đầu tiên mình xin tập trung vào một loại pattern mà chắc anh em ai cũng nghe qua, đó là Factory Pattern

    Factory Pattern có 3 loại: Simple factory, Factory method và Abstract Factory

    Bài lần này mình sẽ trình bày về Simple Factory Pattern

    Simple Factory Pattern

    Bài toán

    Nào chúng ta hãy làm quen với Simple Factory Pattern với một ví dụ như sau:

    Giả sử bạn đang viết chương trình đặt hàng và ship hàng cho một cửa hàng Pizza

    Quá trình đặt hàng một chiếc Pizza sẽ qua các công đoạn như chuẩn bị (prepare), nướng (bake) và đóng hộp (box)

    Xử lý để chạy được & vấn đề

    Chương trình có thể được viết như sau

    Class PizzaStore {
      public Pizza orderPizza() {
        Pizza pizza = new Pizza();
        pizza.prepare();
        pizza.bake();
        pizza.box();
        return pizza;
      }
      public void shipPizza() {
        Pizza pizza = new Pizza();
        pizza.ship();
       }
    }
    

    OK như vậy là có sample về chương trình orderPizza và shipPizza.

    Nhưng chủ cửa hàng lại muốn có nhiều món pizza cơ mà.

    Ví du: Pizza gà (ChickenPizza), Pizza phô mai(CheesePizza)

    Bạn nên làm thế nào ???

    Dễ mà tạo một lớp cha Pizza và create 2 lớp con là ChickenPizza và CheesePizza

    Sau đó add thêm parameter type cho orderPizza và shipPizza

    Class PizzaStore {
      public Pizza orderPizza(String type) {
        Pizza pizza;
        if ("chicken".equals(type) {
          pizza = new ChickenPizza();
        } esle ("cheese".equals(type) {
          pizza = new ChickenPizza();
        }
        pizza.prepare();
        pizza.bake();
        pizza.box();
        return pizza;
      }
      public void shipPizza(String type) {
        Pizza pizza;
        if ("chicken".equals(type) {
          pizza = new ChickenPizza();
        } esle if ("cheese".equals(type) {
          pizza = new ChickenPizza();
        }
        pizza.ship();
      }
    }
    

    OK các bạn thấy thế nào, chương trình chạy ngon không có lỗi gì luôn :D.

    Một tuần sau cửa hàng thấy cần thêm món Pizza hải sản (SeaFoodPizza) để tăng thêm khách hàng

    Bạn sẽ vào sửa method orderPizza???

    Class PizzaStore {
      public Pizza orderPizza(String type) {
        Pizza pizza;
        if ("chicken".equals(type) {
          pizza = new ChickenPizza();
        } esle if ("cheese".equals(type) {
          pizza = new ChickenPizza();
        } else if ("seafood".equals(type) {
          pizza = new SeaFoodPizza();
        }
        pizza.prepare();
        pizza.bake();
        pizza.box();
      }
      public void shipPizza(String type) {
        Pizza pizza;
        if ("chicken".equals(type) {
          pizza = new ChickenPizza();
        } esle if ("cheese".equals(type) {
          pizza = new ChickenPizza();
        }
        pizza.ship();
      }
    }
    

    Cơn ác mộng mới chỉ bắt đầu :)). Đấy là mình chỉ liệt kê 2 chức năng cơ bản, điều gì sẽ xảy ra nếu còn rất nhiều chức năng khác cần lấy thông tin pizza theo từng loại

    Ví du: Lấy thông tin giá, tên, … của từng loại Pizza

    Bạn sẽ phải hì hục sửa code tất cả cả các method đấy nếu cửa hàng tạo thêm một loại Pizza.

    Vâng bạn sẽ vẫn cố gắng sửa để nó có thể chạy được nhưng bạn sẽ tốn rất nhiều effort để làm việc này nếu có hàng chục method cần sửa.

    Bạn cũng lo lắng mình có thể quên xử lý ở một method nào đấy,…

    -> Giờ bạn đã sợ maintain chưa

    Áp dụng Simple Factory Pattern vào bài toán

    Quá nhiều điều để lo lắng chúng ta phải refactor thôi.

    Đầu tiên chắc các bạn cũng thấy luôn, chúng ta cần đóng gói việc khởi tạo object bên dưới

    if ("chicken".equals(type) {
      pizza = new ChickenPizza();
    } esle if ("cheese".equals(type) {
      pizza = new ChickenPizza();
    } else if ("seafood".equals(type) {
      pizza = new SeaFoodPizza();
    }
    

    Chuyển đoạn code trên sang một class factory

    Class SimplePizzaFactory {
      public Pizza createPizza(type) {
        Pizza pizza;
        if ("chicken".equals(type) {
          pizza = new ChickenPizza();
        } esle if ("cheese".equals(type) {
          pizza = new ChickenPizza();
        } else if ("seafood".equals(type) {
          pizza = new SeaFoodPizza();
        }
        return pizza;
     }
    }
    

    Tiếp theo chúng ta sẽ sử dụng SimplePizzaFactory để tạo trong PizzaStore để tạo các object

    Class PizzaStore {
      SimplePizzaFactory mFactory;
    
      public PizzaStore (SimplePizzaFactory factory) {
        mFactory = factory;
      }
    
      public Pizza orderPizza(String type) {
        Pizza pizza = mFactory.createPizza(type);
        pizza.prepare();
        pizza.bake();
        pizza.box();
      }
      public void shipPizza(String type) {
        Pizza pizza = mFactory.createPizza(type);
        pizza.ship();
      }
    }
    

    Bạn thấy công việc đơn giản hơn chưa, việc khởi tạo object cần thiết được xử lý trong Factory

    Như vậy chúng ta không cần phải lo lắng sửa code ở từng method như orderPizzahay shipPizza, … nữa 🙂

    Conlution

    Đây chỉ là một ví dụ rất cơ bản về Design Pattern giúp mọi người dễ hình dung và tiếp cận

    Chúng ta còn rất nhiều bài toán phức tạp khác cần Design Pattern để xử lý

    Quan trọng mọi người hiểu được việc "code để chạy" có thể nhanh trong thời điểm đấy

    Tuy nhiên việc mọi người phải rework để phát triển nó sẽ tốn gấp đôi gấp mười lần effort nếu mọi người có thể làm Design cẩn thận từ đầu

    Do đó việc hiểu các Design pattern hỗ trợ rất tốt cho mọi người trong việc triển khai các bài toán cụ thể

    Hy vọng bài này giúp mọi người hiểu được cơ bản Design Pattern là gì và tầm quan trọng của nó trong software

  • Git for fun

    Git for fun

    Một vài tips hay ho khi sử dụng git (sử dụng command-line)

    Đối với Backend dev, việc sử dụng command-line tool với git là khá cần thiết vì có thể môi trường làm việc không có sẵn GUI để sử dụng. Đồng thời sử dụng command-line trong nhiều trường hợp mang lại tốc độ tốt hơn so với sử dụng GUI. Vì thế tui cũng hay hướng dẫn mấy dev mới tập quen dần với git command-line

    Git CLI có mấy tips khá là fun, giúp việc sử dụng git trở nên linh hoạt và dễ dàng

    1. Có khi nào bạn muốn commit 1 empty folder lên source code? Việc này là không thể vì git không nhận empty folder. Nhưng tình huống này rất hay xảy ra (trong trường hợp đang base source). Để giải quyết trường hợp này, rất đơn giản, ta tạo 1 file .gitkeep ko có nội dung và cho vào folder đó rồi commit bình thường
    2. Thi thoảng, khi chúng ta có 1 file trót “commit” lên repo, sau đó mới nhận ra file đó nên được cho vào ignore. Nhưng khi thêm file vào .gitignore thì không thấy nhận??? Vậy phải xử lý như thế nào? Rất đơn giản, bạn cần clear cache của git đi sau đó add lại là ok. Cú pháp
    git rm -rf --cached .
    git add . 
    git commit -m "fix ignore"
    1. git checkout develop > git pull origin develop > git checkout -b hotfix_xxxx > git commit -m "message" Các từ develop, checkout... này cứ lặp đi lặp lại. Có cách nào cho đơn giản không? Hãy dùng git alias. Đây là ví dụ sử dụng:
      git cod
      git ci -m “message”
      git co branch
      git lg
      git sync remote_name

    Và đây là nội dung alias tui hay add vào file .gitconfig

    [alias]
        co = checkout
        br = branch
        ci = commit
        st = status
        cod = "checkout develop"
        pod = "pull origin develop"
        lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative
        sync = !git checkout develop && git pull $1 develop && git push origin develop && :

    Vụ git lg sẽ cho bạn nhìn git log theo dạng branch-tree với color vô cùng dễ nhìn (nhìn như GUI tool luôn):

    1. Có bao giờ bạn vừa làm việc với git3 của fsoft, vừa làm việc với git server của KH? Làm sao để source code trên máy chỉ cần có 1 bộ source, mà vẫn đảm bảo sync được giữa 2 repo git3 và git private của KH? Đó chính là khái niệm của git remote. Chắc ít người để ý đến cái từ origin khi gõ git fetch origin hay git push origin master nhỉ. Mình đã gặp ở khá nhiều dev mới, quen sử dụng GUI và khi cần sync giữa 2 git server khác nhau, các bạn ấy copy source code và push lên. Wtf??? Đầu tiên phải nắm được 1 số khái niệm cơ bản:
      • origin ở đây hiểu nôm na là 1 cái tên của 1 git server, được đặt ra. Default là origin. Tuy nhiên 1 nước không thể có 2 vua, thế nên nếu làm việc với nhiều git server thì sẽ phải dùng các tên khác. Xem ví dụ cho dễ hiểu nhỉ:

    git remote add origin https://git3.fsoft.com.vn/hotgirlxxx

    git remote add git_customer https://github.com/customer_like_girls

    Vậy là chúng ta đã có 2 remote server khác nhau cùng nằm trong folder source code. Lúc nào cần kéo code từ remote A sync sang remote B thì ta chỉ cần git pull remoteA rồi push sang remoteB là được. Và như file alias tui làm ở trên thì đang quy ước origin là remote name của git fsoft, còn xyz là remote name của git KH. Vậy lúc ae trong team hô “xin em phát anh ơi” thì gõ: git sync xyz => Easy game.



    Một vài tips hay ho hơn nữa sẽ được cung cấp sau khi bài viết đủ 100 likes hehehe…

  • Đừng lạm dụng Enum

    Đừng lạm dụng Enum

    Nhà tâm lý học người Mỹ Abraham Maslow có một câu nói rất nổi tiếng

    If you only have a hammer, you tend to see every problem as a nail. (Nếu dụng cụ duy nhất bạn có chỉ là một chiếc búa, thì mọi vấn đề đều trông giống cái đinh)

    Câu nói này rất phù hợp với lập trình. Mỗi vấn đề đều có nhiều cách tiếp cận với ưu nhược điểm riêng tuỳ theo ngữ cảnh và ràng buộc. Không có giải pháp nào luôn hợp lý hoặc luôn tệ trong tất cả các trường hợp, kể cả Singleton ?. Enum cũng vậy. Nó là một tính năng ngôn ngữ linh hoạt và mạnh mẽ, tuy nhiên việc lạm dụng enum không chỉ làm giảm chất lượng code mà còn khiến codebase khó mở rộng hơn.


    Đầu mục bài viết


    1-Bản chất enum

    Trong tài liệu của mình, Apple chỉ ra rằng enum được tạo ra để định nghĩa một type với mục đích chứa các giá trị liên quan tới nhau. Nói cách khác, hãy dùng enum để nhóm một tập giá trị hữu hạn, cố định, và có quan hệ với nhau. Ví dụ như enum định nghĩa phương hướng

    //Tạo enum Direction ở đây là hợp lý bởi
    //các case liên quan tới nhau và số lượng case là hữu hạn
    enum Direction {
        case north
        case south
        case east
        case west
    }
    

    2-Vấn đề của enum

    Một khi được định nghĩa, chúng ta sẽ không thể thêm case để mở rộng enum mà không làm ảnh hướng tới những chỗ nó được sử dụng. Điều này có thể mang lại lợi ích nếu ta không dùng default case bởi Xcode sẽ giúp tránh việc bỏ lọt code. Tuy nhiên, đây cũng là một nhược điểm lớn trong trường hợp code hiện giờ không quan tâm tới case mới đó.

    Dùng enum để model các Error phức tạp

    Chắc hẳn chúng ta đã từng dùng enum để nhóm các loại Error cho response API như dưới đây

    enum APIError: Error {
        case invalidData(Data)
        case parsingError(Error)
        case noConnection
        case unknown(code: Int, description: String)
        //... các case khác ...
    }
    

    Thoạt nhìn, việc tạo APIError là hợp lý bởi chúng đều là lỗi liên quan đến response API và giờ đây ta có thể gõ .parsingError hay .invalidData cực kì tiện lợi. Mặc dù vậy, hướng tiếp cận này có 2 nhược điểm lớn:

    • Ta không bao giờ muốn switch toàn bộ case của nó
    • Nó không phải là cách tối ưu bởi struct là công cụ tốt hơn để giải quyết bài toán này

    Trong quá trình sử dụng, ngoại trừ các case cần thiết của APIError, việc switch toàn bộ case là hơi thừa thãi. Có thể hiện tại ta chỉ quan tâm đến lỗi .noConnection để hiện alert riêng và các lỗi khác sẽ dùng chung một kiểu alert. Cũng có thể ta chỉ quan tâm đến một vài lỗi nhất định để xử lý logic code nhưng chắc chắn không bao giờ là tất cả case cùng lúc. Lý do là bởi ngoài việc cùng miêu tả các error response, các error trên không có quan hệ gì với nhau.


    Hơn nữa, về mặt logic, việc dùng enum ở đây là sai bởi số lỗi có thể xảy ra khi xử lý API là vô hạn. Điều này trái ngược trực tiếp với bản chất của enum được nhắc đến ở trên. Trong trường hợp này, model APIError bằng struct phù hợp hơn rất nhiều

    struct InvalidData: Error {
        let data: Data
    }
    
    struct ParsingError: Error {
        let underlyingError: Error
    }
    
    struct NoConnection: Error { }
    
    struct Unknown: Error {
        let code: Int
        let description: String
    }
    

    Nếu thực sự muốn nhóm các lỗi vào một kiểu Error, chỉ cần tạo riêng một protocol và conform chúng với protocol đó

    protocol APIError: Error { }
    
    extension InvalidData: APIError { }
    extension ParsingError: APIError { }
    extension NoConnection: APIError { }
    extension Unknown: APIError { }
    

    Việc model APIError bằng structprotocol giúp code linh hoạt hơn khi giờ đây việc tạo ra các Error mới không làm ảnh hưởng đến codebase. Ta cũng có thể cung cấp hàm khởi tạo custom cho chúng, hay conform từng lỗi với các protocol khác nhau một cách dễ dàng thay vì những switch statement cồng kềnh trong enum. Cuối cùng, việc thêm và truy cập biến trong struct đơn giản hơn so với associated value trong enum rất nhiều.


    Sử dụng enum để model các Error đơn giản và hữu hạn là điều nên làm. Tuy nhiên, nếu tập Error đó lớn, hoặc chứa nhiều data đính kèm như các lỗi liên quan đến API thì struct là một lựa chọn tốt hơn hẳn. Trong thực tế, Apple cũng chọn cách này khi tạo URLError xử lý cho Networking của Foundation.

    Dùng enum để config code

    Một sai lầm phổ biến nữa là dùng enum để config UIView, UIViewController, hoặc các object nói chung

    enum MessageCellType {
        case text
        case image
        case join
        case leave
    }
    
    extension MessageCellType {
        var backgroundColor: UIColor {
            switch self {
            case .text: return .blue
            case .image: return .red
            case .join: return .yellow
            case .leave: return .green
            }
        }
        
        var font: UIFont {
            switch self {
            case .text: return .systemFont(ofSize: 16)
            case .image: return .systemFont(ofSize: 14)
            case .join: return .systemFont(ofSize: 12, weight: .bold)
            case .leave: return .systemFont(ofSize: 12, weight: .light)
            }
        }
        
        //...
    }
    
    class TextCell: UITableViewCell {
        func style(with type: MessageCellType) {
            contentView.backgroundColor = type.backgroundColor
            textLabel?.font = type.font
        }
    }
    

    MessageCellType định nghĩa các style cho giao diện của cell ứng với từng loại message để tái sử dụng ở nhiều màn khác nhau. Các thuộc tính chung có thể kể đến như backgroundColor hay UIFont.


    Giống với APIError, vấn đề đầu tiên của MessageCellType là ta không muốn switch toàn bộ case của nó. Với mỗi loại cell, ta chỉ muốn dùng một type nhất định để config cell đó. Việc switch tất cả các case ở hàm cellForRow(at:) là không hợp lý bởi luôn phải trả ra fatalError hoặc một UITableViewCell bù nhìn để thoả mãn Xcode vì số lượng subclass của UITableViewCell là vô hạn ?‍♂️.


    Một vấn đề khác với MessageCellType là việc khó mở rộng. Bản chất của enum là tính hoàn thiện và hữu hạn. Khi thêm bất kì case mới nào, ta đều phải update tất cả các switch statement sử dụng nó. Điều này đặc biệt tệ trong trường hợp đang viết framework vì giờ đây thay đổi sẽ phá hỏng code từ phía client.
    Giải pháp cho MessageCellType là biến nó thành struct và tạo ra các biến static thuộc type này

    struct MessageCellType {
        let backgroundColor: UIColor
        let font: UIFont
    }
    
    extension MessageCellType {
        static let text = MessageCellType(backgroundColor: .blue, font: .systemFont(ofSize: 16))
        static let image = MessageCellType(backgroundColor: .red, font: .systemFont(ofSize: 14))
        static let join = MessageCellType(backgroundColor: .yellow, font: .systemFont(ofSize: 12, weight: .bold))
        static let leave = MessageCellType(backgroundColor: .green, font: .systemFont(ofSize: 12, weight: .light))
    }
    

    Refactor từ enum thành struct giúp việc thêm config mới không còn là vấn đề bởi nó không hề ảnh hưởng tới codebase. Một lợi ích nhỏ nữa là ta vẫn được gõ .join hoặc .leave khi truyền chúng vào trong function

    let cell: TextCell = TextCell()
    cell.style(with: .join)
    

    3-Tổng kết

    Trước khi tạo enum, hãy luôn nhớ rằng

    Enum dùng để switch. Nếu không chắc rằng mình muốn switch nó thì hãy sử dụng struct và protocol


  • Design Pattern: Builder Pattern trong iOS

    Design Pattern: Builder Pattern trong iOS

    Design Pattern: Builder pattern trong iOS

    Nghe đến Design Pattern, chắc hẳn mỗi lập trình viên đều biết đến kỹ thuật quan trọng này và đã từng áp dụng nó ít nhất một lần. Design pattern giúp bạn giải quyết vấn đề một cách tối ưu nhất, cung cấp cho bạn các giải pháp trong lập trình hướng đối tượng (OOP). Phần lớn các ngôn ngữ lập trình đều có thể áp dụng Design pattern và Swift cũng không ngoại lệ!

    Design pattern có 3 nhóm chính:

    • Creational Pattern bao gồm: Abstract Factory, Factory Method, Singleton, Builder, Prototype.
    • Structural Pattern bao gồm: Adapter, Bridge, Composite, Decorator, Facade, Proxy và Flyweight.
    • Behavioral Pattern bao gồm: Interpreter, Template Method, Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy và Visitor.

    Hôm nay, chúng ta sẽ cùng tìm hiểu một design pattern trong nhóm Creational Pattern (Nhóm khởi tạo) là Builder pattern.

    Nội dung

    • Builder pattern là gì?
    • Vấn đề
    • Sử dụng nó như thế nào
    • Áp dụng trong iOS
    • Tổng kết

    Builder pattern là gì?

    Trong OOP, Builder pattern là một loại pattern thuộc nhóm Creational pattern (Tạo dựng), vì vậy nó sẽ giúp chúng ta giải quyết các vấn đề liên quan tới tạo dựng một object bằng cách:

    • Chia nhỏ các hàm khởi tạo của một object.
    • Đặt cấu hình mặc định và logic xử lý liên quan tới việc khởi tạo một object từ trong class ra bên ngoài và đẩy vào Builder class.

    Và việc chia nhỏ và đẩy vào Builder class như thế nào thì chúng ta hãy cùng tìm hiểu tiếp về vấn đề và áp dụng Builder Pattern trong một bài toán.

    Vấn đề

    Giả sử, chúng ta có một chương trình đặt bánh pizza, và mục tiêu của chúng ta là tạo ra chương trình đặt bánh để lấy yêu cầu từ khách hàng.

    Chúng ta có đối tượng Pizza, vả khởi tạo các đối tượng Pizza:

    
    class Pizza {
        
        var smell: String
        var base: String
        var size: String
        var isChiliSauce: Bool
        var isKetchup: Bool
        
        init(smell: String, base: String, size: String, isChiliSauce: Bool, isKetchup: Bool) {
            self.smell = smell
            self.base = base
            self.size = size
            self.isChiliSauce = isChiliSauce
            self.isKetchup = isKetchup
        }
    }
    
    
    
    let sizeM = Pizza(smell: "Bò", base: "Mỏng", size: "M", isChiliSauce: true, isKetchup: true)
    
    let sizeMChicken = Pizza(smell: "Gà", base: "Mỏng", size: "M", isChiliSauce: true, isKetchup: true)
    
    let sizeL = Pizza(smell: "Gà", base: "Dày", size: "L", isChiliSauce: true, isKetchup: true)
    
    

    Chúng ta có thể thấy hàm tạo khá nhiều thuộc tính, và có những thuộc tính mặc định ít thay đổi. Vì những thuộc tính đó ít thay đổi nên sẽ không tránh khỏi việc duplicate code. Do đó, người ta sử dụng Builder Pattern.

    Sử dụng nó như thế nào

    Class diagram

    Cùng phân tích một chút nhé:

    • Director: Đại diện cho module cần kết quả từ việc khởi tạo Pizza.
    • PizzaBuilder: Một class implement InterfaceBuilder, có nhiệm vụ khởi tạo ra Pizza, thông qua hàm build().
    • Pizza: Là object cần khởi tạo.

    Demo

    Một protocol nhằm khai báo các phương thức

    
    protocol PizzaBuilderProtocol {
        
        func build() -> Pizza
        
        func setSmell(_ smell: String) -> PizzaBuilder
    
        func setBase(_ base: String) -> PizzaBuilder
    
        func setSize(_ size: String) -> PizzaBuilder
        
        func withChiliSauce(_ isChiliSauce: Bool) -> PizzaBuilder
        
        func withKetchup(_ isKetchup: Bool) -> PizzaBuilder
    }
    
    

    PizzaBuilder triển khai các phương thức đã khai báo ở PizzaBuilderProtocol

    
    class PizzaBuilder: PizzaBuilderProtocol {
        
        private var pizza = Pizza()
        
        func build() -> Pizza {
            return self.pizza
        }
        
        func setSmell(_ smell: String) -> PizzaBuilder {
            pizza.smell = smell
            return self
        }
        
        func setBase(_ base: String) -> PizzaBuilder {
            pizza.base = base
            return self
        }
        
        func setSize(_ size: String) -> PizzaBuilder {
            pizza.size = size
            return self
        }
        
        func withChiliSauce(_ isChiliSauce: Bool) -> PizzaBuilder {
            pizza.isChiliSauce = isChiliSauce
            return self
        }
        
        func withKetchup(_ isKetchup: Bool) -> PizzaBuilder {
            pizza.isKetchup = isKetchup
            return self
        }
        
    }
    
    

    Cart ở đây giữ vai trò tương đương Director trong hình bên trên.

    
    class Cart {
        
        let orderId: String
        let products: [Pizza]
        
        init(orderId: String, products: [Pizza]) {
            self.orderId = orderId
            self.products = products
        }
    }
    
    let pizza1 = PizzaBuilder()
                .setSize("s")
                .setSmell("Gà")
                .setBase("Dày")
                .build()
            
    let cart1 = Cart(orderId: "OrderId1", products: [pizza1])
    
    

    Cách thực hiện thật đơn giản phải không? Bản chất của nó là chia nhỏ các properties trong Object ra thành các hàm getter, setter trong Builder và sau đó thực hiện set giá trị cho chúng và khởi tạo Object qua hàm build(). Và đó chính là cách viết theo Builder Pattern.

    Chắc hẳn khi các bạn đọc đến đây sẽ có một suy nghĩ rằng: Vậy tại sao không dùng default value? Đúng vậy, khi Builder Pattern được giới thiệu, ngôn ngữ họ sử dụng khi đó là C++. Nhưng, Swift của chúng ta đã cung cấp tính năng default value, và chúng ta có thể hoàn toàn loại bỏ Builder. Nói như vậy thì Builder Pattern sẽ trở nên vô dụng trong Swift?

    Chắc chắn là không. Những tài liệu trên mạng về Builder Pattern khá nhiều, và nó đang hướng người đọc vào vấn đề default value trong các ngôn ngữ như: C++, Java…Chúng ta hãy cùng nhìn lại về những thứ mà Builder làm, đó là giảm thiệu sự phức tạp khi khởi tạo thông qua hàm builder(). Bản chất chính là nó đang đóng gói việc khởi tạo một Object. Khi đó, chúng ta sẽ có thể xử lý nhiều hiện object, do something… trước khi trả về một Object hoàn chỉnh.

    Áp dụng trong iOS

    Chúng ta có thể áp dụng Builder Pattern ở rất nhiều trường hợp trong lập trình iOS. Mình, mình áp dụng trong việc customize một style cho Label, Button…

    Thường khi, chúng ta customize một Label theo một style nào đó, chúng ta sẽ thường khởi tạo như thế này, nhìn có vẻ khá rối và không được thú vị cho lắm.

    
    let attrString = NSMutableAttributedString(string: title)
            
    attrString.addAttributes([NSMutableAttributedString.Key.foregroundColor: UIColor.red],
                                      range: attrString.mutableString.range(of: title))
            
    attrString.addAttributes([NSMutableAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 13)],
                                      range: attrString.mutableString.range(of: title))
            
    attrString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: 2,
                                         range: NSMakeRange(0, attrString.length))
            
    titleLabel.attributedText = attrString
    
    

    Chúng ta có thể áp dụng Builder pattern trong trường hợp này để nhìn code trở lên dễ đọc hơn và cũng dễ dàng phát triển trong tương lai.

    Tạo một builder và protocol

    
    protocol AttributedStringBuilderProtocol {
        
        func build() -> NSMutableAttributedString
        func setFont(_ font: UIFont, forSubString subString: String) -> AttributedStringBuilder
        func setColor(_ color: UIColor, forSubString subString: String) -> AttributedStringBuilder
        func withStrikeThrough() -> AttributedStringBuilder
        
    }
    
    class AttributedStringBuilder: AttributedStringBuilderProtocol {
        
        private var attrString: NSMutableAttributedString
        
        init(string: String) {
            self.attrString = NSMutableAttributedString(string: string)
        }
        
        func build() -> NSMutableAttributedString {
            return self.attrString
        }
        
        func setFont(_ font: UIFont, forSubString subString: String) -> AttributedStringBuilder {
            self.attrString.addAttributes([NSMutableAttributedString.Key.font: font],
                                          range: self.attrString.mutableString.range(of: subString))
            return self
        }
        
        func setColor(_ color: UIColor, forSubString subString: String) -> AttributedStringBuilder {
            self.attrString.addAttributes([NSMutableAttributedString.Key.foregroundColor: color],
                                          range: self.attrString.mutableString.range(of: subString))
            return self
        }
        
        func withStrikeThrough() -> AttributedStringBuilder {
            self.attrString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: 2,
                                         range: NSMakeRange(0, self.attrString.length))
            return self
        }
    }
    
    

    Sử dụng

    
    let atrString = AttributedStringBuilder(string: title)
                .setFont(.boldSystemFont(ofSize: 13), forSubString: title)
                .setColor(.red, forSubString: title)
                .withStrikeThrough()
                .build()
    
    titleLabel.attributedText = attrString
    
    

    Như vậy, với trường hợp trên, việc sử dụng Builder Pattern sẽ giúp những dòng code của chúng ta trở nên dễ nhìn hơn và thuận lợi trong việc phát triển tiếp những properties của nó.

    Tổng kết

    Tóm lại, Design pattern: Builder Pattern là một trong những pattern dễ thực hiện. Nhưng khi sử dụng pattern này chúng ta cần phải mất thời gian để xác định được trường hợp nào cần nó, nó có thể trở nên rườm rà hơn khi sử dụng trong Swift. Nhưng chúng ta hãy đọc, tìm hiểu để áp dụng những tư duy và ý tưởng của một pattern vào một bài toán phù hợp. Đầu tư thời gian vào việc chuẩn bị xây dựng một sản phẩm sẽ luôn sẽ giúp việc bảo trì code trở nên dễ dàng hơn.

  • [Flutter] Hướng dẫn tạo plugin và gọi thư viện native (Phần cuối)

    [Flutter] Hướng dẫn tạo plugin và gọi thư viện native (Phần cuối)

    Xem thêm Phần 1-2 Xem thêm Phần 3

    Phần 4: Hướng dẫn thêm thư viện native

    Trong phần này, mình sẽ demo việc gửi 1 DateTime từ flutter xuống native code để kiểm tra xem có phải ngày hiện tại hay không? Mình sẽ sử dụng thư viện Tempo của tác giả cesarferreira cho Android và SwiftDate của tác giả Daniele Margutti cho iOS.

    Vì flutter và native không giao tiếp với nhau bằng biến loại DateTime được, nên mình sẽ cần chuyển DateTime sang dạng string UTC để xử lý nhé.

    Thêm code flutter để hiển thị kết quả

    Trong file lib/src/sample_call_native.dart các bạn thêm 1 hàm như sau:

      static Future isToday(DateTime dateTime) async {
        final date = dateTime.toUtc().toIso8601String();
        final bool? isSuccess = await _channel.invokeMethod(
          'isToday',
          {
            'dateTime': date,
          },
        );
        return isSuccess;
      }
    

    Trong file example/lib/main.dart bạn đổi lại code như sau:

    import 'package:flutter/material.dart';
    import 'package:sample_plugin_flutter/sample_plugin_flutter.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatefulWidget {
      @override
      _MyAppState createState() =&gt; _MyAppState();
    }
    
    class _MyAppState extends State {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(
              title: const Text('Plugin example app'),
            ),
            body: Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  /// Phần 2. Hướng dẫn tạo Widget với plugin
                  SampleButton(
                    text: "Sample Button",
                    onPressed: () {
                      print("Sample Button Click");
                    },
                  ),
    
                  /// Phần 3. Hướng dẫn gọi native code từ plugin
                  FutureBuilder(
                    future: SampleCallNativeFlutter.platformVersion,
                    builder: (_, snapshoot) {
                      return Text(snapshoot.data ?? '');
                    },
                  ),
    
                  /// Phần 4. Hướng dẫn gọi native code từ plugin
                  FutureBuilder(
                    future: SampleCallNativeFlutter.isToday(DateTime.now()),
                    builder: (_, snapshoot) {
                      return Text('isToDay: ${DateTime.now()} is ${snapshoot.data}');
                    },
                  ),
                  FutureBuilder(
                    future: SampleCallNativeFlutter.isToday(DateTime(2021,01,01)),
                    builder: (_, snapshoot) {
                      return Text('isToDay: ${DateTime(2021,01,01)} is ${snapshoot.data}');
                    },
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    

    Thêm thư viện cho iOS

    Thường khi thêm 1 thư viện vào code iOS, bạn cần sử dụng Cocoapods thêm nó vào Podfile. Nhưng với plugin thì bạn sẽ thêm dependency nó vào ios/sample_plugin_flutter.podspec.

    File này cũng giúp bạn khai báo s.static_framework = true(1 số thư viện native cần phải khai báo biến này) hay s.ios.deployment_target = ‘9.0’ (để giới hạn version build iOS).

    (Nếu bạn chưa biết Cocoapods là gì, bạn có thể tham khảo tại đây)

    #
    # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
    # Run `pod lib lint sample_plugin_flutter.podspec` to validate before publishing.
    #
    Pod::Spec.new do |s|
      s.name             = 'sample_plugin_flutter'
      s.version          = '0.0.1'
      s.summary          = 'A new flutter plugin project.'
      s.description      = &lt; '../LICENSE' }
      s.author           = { 'Your Company' =&gt; '[email protected]' }
      s.source           = { :path =&gt; '.' }
      s.source_files = 'Classes/**/*'
      s.dependency 'Flutter'
      s.dependency 'SwiftDate' # Khai báo thư viện iOS tại đây
      s.platform = :ios, '8.0'
    
      # Flutter.framework does not contain a i386 slice.
      s.pod_target_xcconfig = { 'DEFINES_MODULE' =&gt; 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' =&gt; 'i386' }
      s.swift_version = '5.0'
    end
    

    Sau đó bạn cần chạy pod install cho thư mục example/ios và vào Xcode chọn menu Product/Clean Build Folder. Trong file SwiftSamplePluginFlutterPlugin bạn đổi lại code như sau:

    import Flutter
    import UIKit
    import SwiftDate
    
    public class SwiftSamplePluginFlutterPlugin: NSObject, FlutterPlugin {
      public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(name: "sample_plugin_flutter", binaryMessenger: registrar.messenger())
        let instance = SwiftSamplePluginFlutterPlugin()
        registrar.addMethodCallDelegate(instance, channel: channel)
      }
    
      public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch call.method {
        case "getPlatformVersion":
            result("iOS " + UIDevice.current.systemVersion)
        case "isToday":
            isToday(call, result)
        default:
            result(nil)
        }
      }
        
        private func isToday(_ call: FlutterMethodCall,_ result: @escaping FlutterResult) {
            let arguments = call.arguments as! Dictionary
            let dateTime = arguments["dateTime"] as! String;
            // Convert to local
            let localDate = dateTime.toDate(nil, region: Region.current)
            // Check isToday
            let checkToday = localDate?.isToday
            result(checkToday)
        }
    }
    

    Thế là xong bên iOS, giờ qua phần của Android.

    Thêm thư viện cho Android

    Trong Gradle Scripts/build.gradle(Module: android.sample_plugin_flutter) bạn thêm dòng bên dưới ở cuối file và nhấn Sync now

    dependencies {
      implementation 'com.github.cesarferreira:tempo:+'
    }
    

    Sample 5

    Trong file android/src/main/kotlin/com/example/sample_plugin_flutter/SamplePluginFlutterPlugin.kt bạn đổi lại code như sau:

    package com.example.sample_plugin_flutter
    
    import androidx.annotation.NonNull
    import com.cesarferreira.tempo.Tempo
    import com.cesarferreira.tempo.isToday
    
    import io.flutter.embedding.engine.plugins.FlutterPlugin
    import io.flutter.plugin.common.MethodCall
    import io.flutter.plugin.common.MethodChannel
    import io.flutter.plugin.common.MethodChannel.MethodCallHandler
    import io.flutter.plugin.common.MethodChannel.Result
    import java.text.SimpleDateFormat
    import java.util.*
    
    /** SamplePluginFlutterPlugin */
    class SamplePluginFlutterPlugin: FlutterPlugin, MethodCallHandler {
      /// The MethodChannel that will the communication between Flutter and native Android
      ///
      /// This local reference serves to register the plugin with the Flutter Engine and unregister it
      /// when the Flutter Engine is detached from the Activity
      private lateinit var channel : MethodChannel
    
      override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(flutterPluginBinding.binaryMessenger, "sample_plugin_flutter")
        channel.setMethodCallHandler(this)
      }
    
      override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
        when (call.method) {
          "getPlatformVersion" -&gt; result.success("Android ${android.os.Build.VERSION.RELEASE}")
          "isToday" -&gt; isToday(call, result)
          else -&gt; {
            result.notImplemented()
          }
        }
      }
    
      private fun isToday(@NonNull call: MethodCall, @NonNull result: Result) {
        var arguments = call.arguments as Map
        var dateTime = arguments["dateTime"] as String
        var localDate = dateTime.toDate()
        var checkToday = localDate.isToday // library Tempo check isToday
        result.success(checkToday)
      }
    
      private fun String.toDate(dateFormat: String = "yyyy-MM-dd'T'HH:mm:ss", timeZone: TimeZone = TimeZone.getTimeZone("UTC")): Date {
        val parser = SimpleDateFormat(dateFormat, Locale.getDefault())
        parser.timeZone = timeZone
        return parser.parse(this)
      }
    
      override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
      }
    }
    

    Xong rồi, giờ chạy flutter run để xem thành quả cuối cùng thôi nào.

    Sample 6

    Kết thúc

    Hi vọng qua bài viết của mình giúp ích cho các bạn phần nào việc làm qua viết plugin cho Flutter. Mình để link Github ở đây để các bạn tham khảo nha.

    Nguồn tham khảo:

    Bài viết đầy đủ tại Viblo Cảm ơn các bạn đã xem bài viết.

    Tác giả

    Phạm Tiến Dũng [email protected]

  • [Flutter] Hướng dẫn tạo plugin và gọi thư viện native (Phần 3)

    [Flutter] Hướng dẫn tạo plugin và gọi thư viện native (Phần 3)

    Xem lại Phần 1-2

    Phần 3. Hướng dẫn gọi native code từ plugin

    1. Làm việc với IDE native

    Khi làm việc với native code, bạn nên dùng Android Studio khi code Android và Xcode khi code iOS nhé. 2 IDE này sẽ hỗ trợ bạn tốt hơn trong việc báo lỗi và cả debug code.

    • Trong Android Studio bạn mở thư mục example/android/, giao diện cây thư mục trong IDE sẽ như thế này. Sample 2

    • Trong Xcode bạn mở thư mục example/ios/Runner.xcworkspace, giao diện cây thư mục trong IDE sẽ như thế này. Sample 3

    2. Code native cho plugin

    Để gọi native code, bạn sẽ cần sử dụng channel, thường channel nên được đặt cùng tên với tên plugin của bạn. Thông qua channel chúng ta sẽ gọi hàm native và nhận kết quả từ đó.

    Các bạn có thể tham khảo mapping các loại biến giữa các nền tảng tại đây.

    Trong thư mục lib/src các bạn tạo 1 file dart mới và đặt tên là sample_call_native.dart. File này sẽ tạo MethodChannel(‘sample_plugin_flutter’) để liên kết đến native code và hàm platformVersion() để kiểm tra version của thiết bị người dùng.

    import 'dart:async';
    
    import 'package:flutter/services.dart';
    
    class SampleCallNativeFlutter {
      static const MethodChannel _channel =
          const MethodChannel('sample_plugin_flutter');
    
      static Future get platformVersion async {
        final String? version = await _channel.invokeMethod('getPlatformVersion');
        return version;
      }
    }
    

    Trong file lib/src/src.dart các bạn thêm dòng export.

    export 'sample_call_native.dart';
    

    Trong file android/src/main/kotlin/com/example/sample_plugin_flutter/SamplePluginFlutterPlugin.kt đã code demo sẵn channel và cách trả về platformVersion như minh họa phía dưới. Tại hàm onMethodCall, cần kiểm tra tên call.method được gọi là gì và trả về cho flutter kết quả thông qua result.success().

    Lưu ý: nếu bạn gọi 1 function không cần trả kết quả, bạn vẫn phải gọi result.success(null) để báo về cho flutter biết hàm đã thực hiện xong.

    package com.example.sample_plugin_flutter
    
    import androidx.annotation.NonNull
    
    import io.flutter.embedding.engine.plugins.FlutterPlugin
    import io.flutter.plugin.common.MethodCall
    import io.flutter.plugin.common.MethodChannel
    import io.flutter.plugin.common.MethodChannel.MethodCallHandler
    import io.flutter.plugin.common.MethodChannel.Result
    
    /** SamplePluginFlutterPlugin */
    class SamplePluginFlutterPlugin: FlutterPlugin, MethodCallHandler {
      /// The MethodChannel that will the communication between Flutter and native Android
      ///
      /// This local reference serves to register the plugin with the Flutter Engine and unregister it
      /// when the Flutter Engine is detached from the Activity
      private lateinit var channel : MethodChannel
    
      override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(flutterPluginBinding.binaryMessenger, "sample_plugin_flutter")
        channel.setMethodCallHandler(this)
      }
    
      override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
        when (call.method) {
          "getPlatformVersion" -&gt; result.success("Android ${android.os.Build.VERSION.RELEASE}")
          else -&gt; {
            result.notImplemented()
          }
        }
      }
    
      override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
      }
    }
    
    

    Tương tự trong file ios/Classes/SwiftSamplePluginFlutterPlugin.swift đã code demo sẵn channel và cách trả về platformVersion như minh họa phía dưới. Tại hàm handle, cần kiểm tra tên call.method được gọi là gì và trả về cho flutter kết quả thông qua result(). Nếu bạn gọi 1 function không cần trả kết quả, bạn vẫn cần gọi result(nil) để báo về cho flutter biết hàm đã thực hiện xong.

    import Flutter
    import UIKit
    
    public class SwiftSamplePluginFlutterPlugin: NSObject, FlutterPlugin {
      public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(name: "sample_plugin_flutter", binaryMessenger: registrar.messenger())
        let instance = SwiftSamplePluginFlutterPlugin()
        registrar.addMethodCallDelegate(instance, channel: channel)
      }
    
      public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch call.method {
        case "getPlatformVersion":
            result("iOS " + UIDevice.current.systemVersion)
        default:
            result(nil)
        }
      }
    }
    

    Trong file example/lib/main.dart bạn đổi lại code như sau:

    import 'package:flutter/material.dart';
    import 'package:sample_plugin_flutter/sample_plugin_flutter.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatefulWidget {
      @override
      _MyAppState createState() =&gt; _MyAppState();
    }
    
    class _MyAppState extends State {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(
              title: const Text('Plugin example app'),
            ),
            body: Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  /// Phần 2. Hướng dẫn tạo Widget với plugin
                  SampleButton(
                    text: "Sample Button",
                    onPressed: () {
                      print("Sample Button Click");
                    },
                  ),
    
                  /// Phần 3. Hướng dẫn gọi native code từ plugin
                  FutureBuilder(
                    future: SampleCallNativeFlutter.platformVersion,
                    builder: (_, snapshoot) {
                      return Text(snapshoot.data ?? '');
                    },
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    

    Chạy flutter run để xem kết quả thôi nào.

    Sample 4

    Còn tiếp

    Bài viết đầy đủ tại Viblo