Blog

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

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

    Giới thiệu

    Hiện nay tài liệu cho việc tạo plugin cho flutter khá ít, mà tài liệu để plugin gọi xuống các thư viện native càng hiếm hơn nữa. Nên hôm nay mình viết bài này để giúp mọi người dễ dàng hơn trong việc viết plugin với flutter. Cùng bắt đầu thôi nào!

    Phần 1. Hướng dẫn tạo plugin

    Để tạo 1 plugin bạn cần dùng lệnh flutter create –template=plugin

    • Sử dụng tùy chọn –platforms để chỉ định plugin sẽ có những ngôn ngữ nào. Có các tùy chọn như: android, ios, web, linux, macos, windows
    • Sử dụng tùy chọn –org để chỉ định tên miền cho tổ chức của bạn
    • Sử dụng tùy chọn –a để chỉ định ngôn ngữ cho android. Bạn có thể chọn java hoặc kotlin
    • Sử dụng tùy chọn –i để chỉ định ngôn ngữ cho ios. Bạn có thể chọn swift hoặc objc
    • Và cuối cùng sẽ là tên plugin của bạn

    Tham khảo:

    flutter create --org com.example --template=plugin --platforms=android,ios -a kotlin -i swift sample_plugin_flutter
    

    Sau khi thao tác trên bạn sẽ có 1 plugin trong thư mục sample_plugin_flutter với một số file cơ bản sau:

    • lib/sample_plugin_flutter.dart => API Dart cho plugin. File này dùng để kết nối các thành phần của plugin, kết nối với native code
    • android/src/main/kotlin/com/example/sample_plugin_flutter/SamplePluginFlutterPlugin.kt => Triển khai API plugin trong Kotlin dành riêng cho nền tảng Android.
    • ios/Classes/SwiftSamplePluginFlutterPlugin.swift => Triển khai API plugin trong Swift dành riêng cho nền tảng iOS.
    • example/ => Một ứng dụng Flutter phụ thuộc vào plugin và minh họa cách sử dụng nó.
    • lib/src/ => Thư mục này sẽ không có sẵn, nhưng bạn cần tạo thư mục này để chứa các file private. Bạn chỉ public các file cần thiết thông qua khai báo export trong lib/sample_plugin_flutter.dart

    Phần 2. Hướng dẫn tạo Widget với plugin

    Để tạo Widget hay Function để người dùng plugin để thể gọi và sử dụng, bạn cần đưa file đó vào thư mục src và export nó ra ngoài. Khi làm vậy, người dùng chỉ cần import 1 dòng duy nhất là có thể sử dụng plugin của bạn.

    Trong thư mục lib/src các bạn tạo 1 file dart mới và đặt tên là sample_button.dart

    import 'package:flutter/material.dart';
    
    class SampleButton extends StatelessWidget {
      final String text;
      final VoidCallback? onPressed;
    
      const SampleButton({
        Key? key,
        required this.text,
        this.onPressed,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return TextButton(
          onPressed: onPressed,
          child: Container(
            padding: EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Colors.blue,
              borderRadius: BorderRadius.circular(10),
            ),
            child: Text(
              text,
              style: TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        );
      }
    }
    

    Trong thư mục lib/src các bạn tạo thêm file src.dart, file này sẽ chứa tất cả file mà bạn muốn export ra ngoài.

    export 'sample_button.dart';
    

    Trong file lib/sample_plugin_flutter.dart bạn nên xóa hết code mặc định đi. File này các bạn sẽ chứa những file bạn muốn export hoặc export những plugin khác có trong dependence của bạn.

    export 'src/src.dart';
    

    Giờ thì thử build Widget này lên từ app example nhé. 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() => _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");
                    },
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    

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

    Sample 1

    Còn tiếp

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

  • Hướng dẫn sử dụng Project Lombok

    Hướng dẫn sử dụng Project Lombok

    Hướng dẫn sử dụng Project Lombok

    Chinh chiến với Java nhiều năm, bạn có cảm thấy nhàm chán khi làm việc với những đoạn code theo khuôn mẫu của nó hay lười biếng phải khai báo các phương thức Getter, Setter cho các class Java? Nếu câu trả lời là có thì hãy sử dụng Project Lombok. Vậy Project Lombok là gì?

    • Lombok?
    • Cài đặt
    • Sử dụng Lombok
    • Lời kết

    Lombok?

    Lombok là một thư viện, một plugin, giúp chúng ta giảm thiểu các đoạn code thừa (boilerplate) bằng cách tự động sinh ra các hàm GetterSetterConstructor, v.v..

    Lombok giúp chúng ta generate code một cách tự động nhưng không giống như cách các IDE làm cho chúng ta. Các IDE generate các phương thức Getter, Setter và một số phương thức khác trong các tập tin .java. Lombok cũng generate các phương thức đó nhưng là trong các tập tin .class file. Không những làm cho code sáng sửa mà còn trông rất hợp lý, dễ quản lý hơn giúp developer tập trung vào tầng nghiệp vụ và logic thay vì mất thời gian làm những việc thừa thãi.

    Cài đặt

    Để sử dụng Lombok trong project ta cần:

    Bước 1

    Thêm lib vào project bằng Maven hoặc Gradle

    Maven

    <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.20</version>
        <scope>provided</scope>
    </dependency>
    

    Gradle

    
    // https://mvnrepository.com/artifact/org.projectlombok/lombok
    compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.20'
    

    Bước 2

    Đến đây có thể bạn sẽ thắc mắc tại sao đã thêm Lombok vào project rồi mà còn phải cài thêm Lombok Plugin vào IDE nữa???

    Ví dụ bạn muốn sử dụng Lombok để generate Get/Set thì nó sẽ tự động thêm code vào class đó trước khi thành file .jar. Nhưng các IDE thì chỉ nhìn thấy các dòng code hiện tại của bạn và tham chiếu tới nó, điều này sẽ dẫn đến những thông báo lỗi khi bạn sử dụng hàm Get/Set này.

    Nên để IDE hiểu rằng các class đã có các hàm Get/Set rồi, thì bạn phải cài thêm Lombok Plugin.

    Bây giờ ta sẽ cài đặt cho IntelliJ IDEA

    Với IntelliJ IDEA version 2020.3 trở lên thì IDE đã hỗ trợ Lombok mà ko cần cài plugin(phần hướng dẫn này cho version trước 2020.3 )

    Vào File -> Setting -> Plugin …

    Search “Lombok”, chọn Lombok Plugin và Install.

    Sử dụng Lombok

    Lombok dùng các Annotation để khai báo

    @Data

    Ví dụ này 2 đoạn code sẽ tương đương

    public class User {
    
        private String name;
    
        private int age;
    
        public User() {
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "User: " + name
                    + " - " + age;
        }
    }
    
    import lombok.Data;
    
    @Data
    public class User {
    
        private String name;
    
        private int age;
    }
    

    Khi bạn đánh dấu 1 class là @Data, thì nó sẽ generate ra Constructor rỗng hoặc có tham số theo yêu cầu, toàn bộ Get/Set, hàm equals, hashCode, toString().

    @NoArgsConstructor@RequiredArgsConstructor@AllArgsConstructor

    Các annotation được dùng trong trường hợp bạn muốn định nghĩa các Contructor theo yêu cầu khác nhau:

    • @NoArgsConstructor: Hàm khởi tạo rỗng, đã đề cập ở trên
    • @RequiredArgsConstructor: Hàm khởi tạo chứa tất cả thuộc tính, ví dụ Champion(String name, String type)
    • @AllArgsConstructor: Hàm khởi tạo theo yêu cầu. Bạn chỉ muốn hàm khởi tạo có vài thuộc tính do bạn chọn thôi, thì bạn thêm final trước thuộc tính trong class, nó sẽ tự sinh ra Constructor như thế.

    @Getter@Setter

    Được sử dụng trong trường hợp chỉ muốn generate Get/Set và không muốn dùng @Data vì có chức năng không cần thiết.

    import lombok.Getter;
    import lombok.Setter;
    
    @Getter
    @Setter
    public class User {
    
        private String name;
    
        private int age;
    }
    

    @Builder

    Thông thường, khi chúng ta cần khởi tạo một đối tượng với rất nhiều thông tin, chúng ta có thể sử dụng Builder Pattern để làm điều này.

    Ví dụ với class User như sau:

    public class Users {
    
        private String name;
    
        private int age;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "User: " + name
                    + " - " + age;
        }
    }
    

    Để tạo đối tượng User với tất cả các thông tin sử dụng Builder Pattern, chúng ta cần tạo một đối tượng UserBuilder như sau:

    public class UserBuilder {
    
        private Users user;
    
        public UserBuilder() {
            user = new User();
        }
    
        public UserBuilder name(String name){
            user.setName(name);
            return this;
        }
    
        public UserBuilder age(int age){
            user.setAge(age);
            return this;
        }
    
        public Users build(){
            return user;
        }
    }
    

    Và dùng UserBuilder này:

    Users users = new UserBuilder()
                    .name("CuongNM")
                    .age(20)
                    .build();
    

    Chắc hẳn ai cũng ngại khi viết 1 class Builder cổ điển như trên, tự dưng phải tạo thêm 1 class nữa, gấp đôi số lượng thuộc tính khai báo, gấp đôi số hàm cần viết.

    Với Lombok vấn đề này đc giải quyết rất nhanh gọn

    @Data
    @Builder
    public class Users {
    
        private String name;
    
        private int age;
    }
    
    Users users = Users.builder()
                    .name("CuongNM")
                    .age(20)
                    .build();
    

    Logging với Project Lombok(@Slf4j)

    Thông thường, khi chúng ta sử dụng các Logging frameworks như Log4J, Logback hay Simple Logging Facade for Java (SLF4J), ta sẽ khai báo như sau:

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
     
    public class Example {
     
        public static final Logger LOGGER = LoggerFactory.getLogger(Example.class);
     
        public void print(String message) {
            LOGGER.info(message);
        }
    }
    

    Nhưng đối với Lombok, ta chỉ cần dùng annotation dành cho Logging framework mà chúng ta muốn sử dụng mà thôi, sau đó thì có thể sử dụng như bình thường. Như ví dụ ở đây, mình đang sử dụng Simple Logging Facade for Java(SLF4J) để logging thì mình sẽ khai báo annotation @Slf4j của Lombok dành cho thư viện này như sau:

    import lombok.extern.slf4j.Slf4j;
     
    @Slf4j
    public class Example {
     
        public void print(String message) {
            log.info(message);
        }
    }
    

    Lời kết

    Lombok sinh ra để giúp cho code của chúng ta trở nên ngắn gọn dễ hiểu hơn và nó còn rất nhiều tính năng hay ho khác. Trong khuôn khổ bài viết này mình chỉ giới thiệu về các tính năng cơ bản của Lombok và được mình sử dụng nhiều nhất trong suốt quá trình làm việc.

    Cảm ơn các bạn đã đọc bài viết! Các bạn có thể tìm hiểu thêm tại trang chủ của Lombok.

    Tác giả

    [email protected]

  • Custom navigation trainsition iOS

    Custom navigation trainsition iOS

    Là một Developer iOS thì ai cũng quen với UINavigationController và chúng ta thường quen với các animation default. Nhưng với một số thiết kế đặc biệt chúng ta cần phải custom animation cho view controller transition.

    Để custom view controller transition bạn cần hiểu về UIViewControllerAnimatedTransitioning

    I. Overview UIViewControllerAnimatedTransitioning

    UIViewControllerAnimatedTransitioning là một protocol cho phép bạn implement các animation cho custom view controller transition

    Để implement protocol này thì có 2 việc ta cần phải làm

    • Xác định duration cho animation bằng func
    func transitionDuration( using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
    • Thực hiện animation traisition cho custom view controller
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
    • UIViewControllerContextTransitioning bao gồm tất cả thông tin về transitioning như: toView, fromView, containerView…

    Như vậy để custom view controller transition nó sẽ gói gọn trong func animateTransition

    II. Sử dụng UIViewControllerAnimatedTransitioning cho custom view controller transition

    Ở đây tôi sẽ hướng dẫn các bạn thực hiện custom animation push và pop thành animation của present(push -> bottom to top và pop -> top to bottom)

    1. Tạo enum cho NavigationTransitionStyle

    enum NavigationTransitionStyle {
        case nomal
        case bottom
    }
    • nomal: transition mặc định
    • bottom: custom animation transition từ bottom to top cho push và ngược lại cho pop

    2. Tạo class implement UIViewControllerAnimatedTransitioning

    Để hỗ trợ cho pop và push ta cần define các variable như sau:

    class CustomNavigationAnimationTransition: NSObject, UIViewControllerAnimatedTransitioning {
        var popStyle: Bool = false
           var navigationStyle: NavigationTransitionStyle = .nomal
    }
    • popStyle: flag để xác định là pop hay push
    • navigationStyle: xác định animation dạng nomal hay bottom

    2.1 Cài đặt duration cho animation

        func transitionDuration( using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return 0.5
        }

    2.2 Cài đặt method animationTransition cho view controller

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    }
    • Điều hướng nếu là pop thì chúng ta sẽ xử lý animation pop ở method animatePop
    if popStyle {
        animatePop(using: transitionContext)
        return
    }
    • Thực hiện get các variable cần thiết để animation view controller: fromView, toView, frameTransition, frameOffset
    let fromView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
    let toView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
    let frameTransition = transitionContext.finalFrame(for: toView)
    var frameOffset = frameTransition.offsetBy(dx: 0, dy: frameTransition.height)
    if navigationStyle == .nomal {
         frameOffset = frameTransition.offsetBy(dx: frameTransition.width, dy: 0)
    }
    • Set frame cho toView và insert toView, fromView vào trong containerView
    toView.view.frame = frameOffset
    transitionContext.containerView.insertSubview(toView.view, aboveSubview: fromView.view)
    • Thực hiện animation bottom to top cho push
    UIView.animate( withDuration: transitionDuration(using: transitionContext),
                    animations: {
                        toView.view.frame = frameTransition
                    },
                    completion: {_ in
                        transitionContext.completeTransition(true)
                    })

    2.3 Cài đặt method animatePop

    Tương tự như ở method animationTransition ở đây ta sẽ bỏ bước set frame cho toView và thực hiện animation cho fromView

    func animatePop(using transitionContext: UIViewControllerContextTransitioning) {
    
            let fromView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
            let toView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
    
            let frameTransition = transitionContext.initialFrame(for: fromView)
            var frameOffset = frameTransition.offsetBy(dx: 0, dy: frameTransition.height)
            if navigationStyle == .nomal {
                frameOffset = frameTransition.offsetBy(dx: frameTransition.width, dy: 0)
            }
            transitionContext.containerView.insertSubview(toView.view, belowSubview: fromView.view)
    
            UIView.animate( withDuration: transitionDuration(using: transitionContext),
                            animations: {
                                fromView.view.frame = frameOffset
                            },
                            completion: {_ in
                                transitionContext.completeTransition(true)
                            })
    }

    3. Sử dụng CustomNavigationAnimationTransition trong view controller

    • Khỏi tạo variable custom animation bên trên
    private let navigationAnimationTransition = CustomNavigationAnimationTransition()
    • Sau đó config navigationStyle cho view controller và set delegate cho navigation controller
    navigationAnimationTransition.navigationStyle = .bottom
    navigationController?.delegate = self
    • Implement UIViewControllerTransitioningDelegate trong view controller
    extension ViewController: UIViewControllerTransitioningDelegate {
        func navigationController(
            _ navigationController: UINavigationController,
            animationControllerFor operation: UINavigationController.Operation,
            from fromVC: UIViewController,
            to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            navigationAnimationTransition.popStyle = (operation == .pop)
            return navigationAnimationTransition
    
        }
    }

    Như vậy chúng ta vừa mới hoàn thành custom view controller transition. Ngoài cách tạo animation như trên thì bạn có thể làm bất cứ dạng animation nào mà bạn muốn.

    Tài liệu tham khảo: https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning

  • Docker thường thức – Phần 1: Giới thiệu về Containers, Virtual machines và Docker

    Docker thường thức – Phần 1: Giới thiệu về Containers, Virtual machines và Docker

    Docker thường thức – Phần 1: Giới thiệu về Containers, Virtual machines và Docker

    Dù bạn là một Kỹ sư phần mềm hay một nhà Khoa học dữ liệu, dù bạn đang lập trình Web hay đang lập trình Mobile,… thì ít nhiều đã nghe nói về Docker. Theo một thống kê của Stack Overflow trong năm 2020; Docker xếp hạng thứ 2 trong số các nền tảng được yêu thích nhất, đồng thời dẫn đầu trong số các nền tảng mà các developers muốn tìm hiểu nhất. Như vậy có thể thấy, Docker đang dần trở thành xu thế tất yếu mà bất kỳ ai tham gia vào ngành công nghiệp công nghệ thông tin cũng nên tìm hiểu, để biết cách sử dụng nó, áp dụng nó, và biến nó trở thành công cụ hữu ích cho các dự án.

    Chuỗi bài viết về chủ đề Docker của tôi nhằm mang lại cho người đọc mới bắt đầu tìm hiểu về docker có những cái nhìn tổng quan, cơ bản nhất xung quanh công nghệ đang là hot trend này, từ đó họ có thể dễ dàng chuyển sang các chủ đề chuyên sâu hơn mà phù hợp với dự án của mỗi người.

    Trong phần 1, mục đích của bài viết này nhằm mang lại cho bạn đọc các khái niệm cơ bản liên quan đến Docker, chúng sẽ là những thứ mà bạn nhất định phải biết nếu muốn đi sâu vào công nghệ này, ngoài những giải thích bằng lời, tôi cũng cung cấp các hình ảnh trực quan cùng với các ví dụ dễ hiểu.

    Công nghệ đến rồi công nghệ đi, nhưng cái nhìn sâu sắc là ở lại trong ta mãi mãi!

    Danh mục nội dung

    Tại sao lại cần Docker?

    Trước khi đi vào các khái niệm cụ thể, tôi muốn đưa bạn qua một ví dụ để bạn có thể hình dung ra Docker hữu ích như thế nào. Giả sử như bạn đang xây dựng một hệ thống end-to-end với tech stack bao gồm: web server sử dụng NodeJs, database sử dụng MongoDB, messaging system sử dụng Redis, và một orchestration tool.

    Cách tiếp cận truyền thống

    Với các cách tiếp cận truyền thống (mà không sử dụng Docker), chúng ta sẽ gặp phải nhiều vấn đề với tech stack nói trên – chúng là các components/services khác nhau trong hệ thống cần xây dựng.

    • Đầu tiên là khả năng tương thích (compatibility) với hệ điều hành nền tảng (underlying os), chúng ta phải đảm bảo rằng tất cả các services khác nhau nói trên tương thích với OS mà chúng ta đang sử dụng. Trong trường hợp tồn tại một service trong tech stack đó có version mà chúng ta định sử dụng không tương thích với OS, thì chúng ta lại phải tìm kiếm OS khác tương thích với tất cả các services để phù hợp với nhu cầu của hệ thống ta muốn xây dựng.

    • Thứ 2 là chúng ta phải kiểm tra khả năng tương thích giữa các services khác nhau này với các libraries và các dependencies trên OS. Sẽ có vấn đề nếu như một service yêu cầu một phiên bản của dependent library, trong khi đó một service khác lại yêu cầu một phiên bản khác của dependent library đó.

    Kiến trúc của hệ thống sẽ thay đổi qua thời gian, chúng ta sẽ phải upgrade lên các phiên bản mới hơn của các thành phần này. Và mỗi lần có một thứ gì đó cần thay đổi, chúng ta lại phải kiểm tra khả năng tương thích của nó với underlying infrastructure (OS, libraries, dependencies,…). Hơn nữa, mỗi khi có một developer mới onboard, chúng ta sẽ gặp khó khăn khi set up môi trường làm việc cho developer đó.

    The Matrix from Hell !!

    Tất cả những vấn đề nói trên dẫn đến việc developing, building và shipping hệ thống gặp nhiều khó khăn.

    Cách tiệp cận sử dụng Docker

    Như vậy, chúng ta cần một cái gì đó có thể giúp chúng ta giải quyết vấn đề về tính tương thích giữa các thành phần này với OS, chúng ta cũng cần một thứ gì đó có thể giúp chúng ta sửa đổi hoặc thay đổi các thành phần này mà không ảnh hưởng đến các thành phần khác trong hệ thống. Docker đã giải quyết tốt cho chúng ta.

    The services with containers

    Với Docker, ta có thể chạy mỗi component trong một container riêng biệt với các dependencies và các libraries của riêng nó, tất cả trên một VM (Virtual machine) và một OS nhưng tách biệt môi trường. Chúng ta chỉ phải build cấu hình Docker một lần và các developer có thể bắt đầu với một lệnh docker run đơn giản, không phân biệt underlying operating system mà chúng ta đang chạy. Tất cả những cái chúng ta cần là có Docker đã được cài đặt.

    Containers vs Virtual Machines

    Cả Containers và Virtual Machines (VMs) đều giống nhau ở mục tiêu, đó là: cô lập môt ứng dụng và các dependencies của nó thành một đơn vị khép kín (self-contained unit) mà có thể chạy ở bất cứ đâu, bất cứ môi trường nào.

    Hơn nữa, các Containers và VMs loại bỏ nhu cầu về phần cứng vật lý, cho phép sử dụng hiệu quả hơn các tài nguyên máy tính, cả về tiêu thụ năng lượng và hiệu quả chi phí.

    Sự khác biệt chính giữa Containers và VMs là ở cách tiếp cận kiến trúc của chúng.

    Virtual machines

    VMs về cơ bản là một sự mô phỏng của một máy tính thực, thực thi các chương trình giống như một máy tính thực.

    VMs chạy trên một máy vật lý sử dụng cái gọi là hypervisor. Một hypervisor chạy trên một host machine hoặc trên một bare-metal.

    • Một hypervisor là một phần của software, firmware hoặc hardware mà VMs chạy trên đó. Bản thân các hypervisors chạy trên một máy tính vật lý, được gọi là host machine. Host machine cung cấp các tài nguyên cho VMs bao gồm RAM và CPU. Các tài nguyên này được phân chia giữa các VMs và có thể được phân phối khi bạn cảm thấy phù hợp. Vì vậy, nếu một VM đang chạy một ứng dụng cần nhiều tài nguyên hơn thì bạn có thể phân bổ nhiều tài nguyên hơn cho VM đó so với các VM khác đang chạy trên cùng một host machine.

    • VM đang chạy trên một host machine thường được gọi là guest machine. Guest machine này chứa ứng dụng và cả những thứ khác nó cần để chạy ứng dụng đó. Nó chiếm hữu toàn bộ phần cứng được ảo hóa của riêng nó, bao gồm virtualized network adapters, storage, và CPU,… và nó cũng có một guest operating system của riêng nó. Nhìn từ bên trong, guest machine hoạt động riêng biệt với các tài nguyên chuyên dụng dành cho nó. Nhìn từ bên ngoài, nó là một máy ảo – chia sẻ tài nguyên được cung cấp bởi host machine.

    Như đã đề cập ở trên, một guest machine có thể chạy trên một hosted hypervisor hoặc trên một bare-metal hypervisor. Có một vài khác biệt quan trọng giữa chúng:

    • Đầu tiên, một hosted hypervisor chạy trên OS của một host machine. Lấy ví dụ, một máy tính đang chạy OSX có thể có một VM được cài đặt trên OS đó. VM không có quyền truy cập trực tiếp vào hardware, nó phải thông qua OS của host machine. Lợi ích của hosted hypervisor đó là underlying hardware đóng vai trò ít quan trọng. OS của máy chủ chịu trách nhiệm về các hardware drivers thay vì chính hypervisor, và do đó có khả năng tương thích phần cứng tốt. Mặc khác, tầng trung gian này (OS của máy chủ) giữa hardware và hypervisor tạo ra nhiều tài nguyên hơn, làm giảm performance của VM.

    • Một bare-metal hypervisor giải quyết vấn đề hiệu năng nói trên bằng cách cài đặt và chạy ngay trên hardware của host machine. Vì nó giao tiếp trực tiếp với phần cứng nên nó không cần OS của máy chủ để chạy. Trong trường hợp này, thứ đầu tiên được cài đặt trên máy chủ đó là hypervisor. Không giống như hosted hypervisor, một bare-metal hypervisor có device drivers của riêng nó và tương tác trực tiếp với từng thành phần cho bất kỳ tác vụ cụ thể nào. Điều này dẫn đến performance, scalability, và stability tốt hơn. Sự đánh đổi ở đây là khả năng tương thích phần cứng bị hạn chế bởi vì hypervisor chỉ có thể có rất nhiều device drivers được tích hợp trong đó.

    Sau tất cả những gì đã nói về hypervisor, bạn có thể sẽ hỏi rằng tại sao chúng ta lại cần thêm một layer "hypervisor" giữa VM và host machine ? Câu trả lời đó là vì VM có một virtual OS của riêng nó, hypervisor đóng vai trò thiết yếu trong việc cung cấp cho VMs một platform để quản lý để thực thi hệ điều hành khách này. Nó cho phép các host computers chia sẻ tài nguyên của chúng tới các VMs đang chạy với tư cách là guest trên chúng.

    Virtual machines

    Như bạn có thể thấy trên diagram, hypervisor đóng gói virtual hardware, kernel và user space cho mỗi VM mới.

    Containers

    Containers có một lịch sử lâu đời trong lĩnh vực điện toán. Không giống như hypervisor virtualization, nơi mà có một hoặc nhiều VMs độc lập chạy ảo trên physical hardware qua một layer trung gian; thay vào đó, containers chạy trong user space ở trên kernel của OS. Do đó, container virtualization thường được gọi là ảo hóa cấp OS. Công nghệ container cho phép nhiều phiên bản user space độc lập được chạy trên một máy chủ duy nhất.

    Do trạng thái của chúng là guest của OS, nên các containers đôi khi được xem là kém linh hoạt hơn: chúng thường chỉ có thể chạy cùng hệ điều hành hoặc hệ điều hành khách tương tự trên máy chủ. Lấy ví dụ: bạn có thể chạy Redhat Enterprise Linux trên một máy chủ Ubuntu, nhưng bạn không thể chạy Microsoft Windows trên máy chủ Ubuntu. Containers cũng được coi là kém an toàn hơn so với sự cô lập hoàn toàn của hypervisor virtualization.

    Bất chấp những hạn chế này, các containers đã được triển khai dưới nhiều user cases khác nhau. Chúng phổ biến cho việc triển khai hyperscale của các dịch vụ multi-tenant, cho lightweight sandboxing,… bất chấp những lo ngại về bảo mật của chúng.

    Các công nghệ container gần đây bao gồm OpenVZ, Solaris Zones và Linux containers như lxc. Sử dụng các công nghệ mới này, containers giờ đây có thể được xem như là một máy chủ hoàn chỉnh theo đúng nghĩa của chúng thay vì chỉ là môi trường thực thi. Trong trường hợp của Docker, vì có các features hiện đại của Linux kernel, như là control group và namespaces –> các containers có sự độc lập mạnh mẽ, network và storage stacks của riêng chúng, cũng như khả năng quản lý tài nguyên để cho phép sự tồn tại thân thiện của nhiều containers trên cùng một máy chủ.

    Mặc dù các containers trước đây đã không đạt được large-scale adoption. Một phần lớn của vấn đề này có thể nằm ở độ phức tạp của chúng: các containers có thể phức tạp, khó để set up, khó để quản lý và tự động hóa. Docker nhằm mục đích thay đổi điều đó.

    Containers

    Tóm lại, một khác biệt lớn giữa containers và VMs đó là các containers share kernel của máy chủ với các containers khác.

    Containers vs VMs

    Dưới đây là một sự so sánh những điểm khác biệt chính giữa VMs và Containers.

    Containers vs VMs

    Ở bên phải hình trên, trong trường hợp của Docker, ta có underlying hardware infrastructure, OS ở phía trên và Docker được cài đặt trên OS, Docker sau đó quản lý các containers – chúng hoạt động cùng với các libraries và dependencies.

    Trong trường hợp của VMs, ở phía bên trái, ta có Hypervisor ở trên hardware và sau đó là VMs ở trên chúng. Và như bạn nhìn thấy, mỗi VMs có OS riêng bên trong nó, sau đó là các libs và deps rồi đến Application –> việc sử dụng các tài nguyên phần cứng trở nên lớn hơn khi có nhiều virtual operating systems đang chạy. VMs cũng tiêu thụ không gian đĩa lớn hơn vì mỗi VM khá nặng – thường lên đến hàng GB, trong khi đó docker containers nhẹ hơn – thường chỉ đến hàng MB –> điều này cho phép Docker container khởi động nhanh hơn – thường chỉ trong vài giây, trong khi đó các VMs sẽ mất khoảng vài phút để khởi động vì nó cần khởi động toàn bộ cả Operating System.

    Một điều quan trọng cần lưu ý là Docker có ít sự cô lập (isolation) hơn vì có nhiều tài nguyên được chia sẻ giữa các containers, như Kernel. Trong khi đó, VMs có sự cô lập hoàn toàn với nhau vì chúng không dựa trên underlying os hay kernel.

    Mặc dù có sự khác biệt nhất định giữa Container và VM, nhưng sự kết hợp giữa chúng có thể mang lại sự hữu ích nếu biết cách phối hợp chúng hợp lý.

    Containers and VMs

    Khi bạn có môi trường lớn với hàng ngàn containers chạy trên hàng ngàn docker host, bạn sẽ thường thấy các containers được cung cấp trên các virtual docker host. Bằng cách này, chúng ta có thể tận dụng được ưu điểm của cả 2 công nghệ, chúng ta có thể sử dụng virtualization để dễ dàng kiểm soát các docker host theo nhu cầu, đồng thời tận dụng Docker để dễ dàng kiểm soát các ứng dụng và scale chúng theo nhu cầu.

    Giới thiệu về Docker

    Đến đây thì chắc hẳn nhiều bạn đọc đặt câu hỏi, vậy tóm lại Docker là gì? Tôi sẽ trả lời câu hỏi này ngay sau đây.

    Docker là một open-source engine, tự động hóa việc triển khai các ứng dụng vào các containers. Nó được viết bởi một team tại Docker, Inc và được họ phát hành theo giấy phép Apache 2.0.

    Vậy Docker có gì đặc biệt? Docker thêm một công cụ triển khai ứng dụng trên môi trường thực thi container đươc ảo hóa. Nó được thiết kế để cung cấp môi trường nhẹ và nhanh chóng để chạy code của bạn, cũng như workflow hiệu quả để đưa code từ laptop –> môi trường test –> môi trường production.

    Docker cực kỳ đơn giản. Thật vậy, bạn có thể bắt đầu với Docker trên một máy chủ minimal không chạy gì ngoài một Linux kernel tương thích và một Docker binary. Nhiệm vụ của Docker là cung cấp:

    Một cách dễ dàng và nhẹ nhàng để mô hình hóa thực tế:

    Docker rất nhanh, bạn có thể Dockerize ứng dụng của mình trong vài phút. Docker dựa trên mô hình copy-on-write nên việc thực hiện các thay đổi đối với ứng dụng của bạn cũng cực kỳ nhanh chóng.

    Sự phân chia logic giữa các tác vụ:

    Với Docker, các Developers quan tâm về các ứng dụng của họ chạy trong các containers và các Operations quan tâm đến việc quản lý các containers. Docker được thiết kế để tăng cường tính nhất quán bằng cách đảm bảo môi trường mà các developers viết code khớp với môi trường mà các ứng dụng được triển khai thực tế. Điều này làm giảm rủi ro: "worked in dev, now an ops problem".

    Vòng đời phát triển nhanh và hiệu quả:

    Docker nhằm mục đích giảm thời gian trong chu kỳ giữa code được viết, code được kiểm tra, code được triển khai và được sử dụng. Nó cũng làm cho các ứng dụng của bạn trở nên khả chuyển, dễ để build, và dễ cộng tác.

    Khuyến khích kiến trúc hướng dịch vụ (SOA):

    Docker cũng khuyến khích các kiến trúc service-oriented và microservices. Docker recommends rằng mỗi container nên chạy một process hoặc một application duy nhất. Điều này thúc đẩy mô hình ứng dụng phân tán, trong đó một ứng dụng hoặc dịch vụ được đại diện bởi một loạt các containers được kết nối với nhau. Điều này giúp dễ dàng distribute, scale, debug và introspect các ứng dụng.

    Các khái niệm cơ bản

    Tôi sẽ không nhắc lại khái niệm về VMs và Containers nữa; mà thay vào đó, tôi sẽ đề cập đến 3 khái niệm quan trọng khác mà bạn sẽ gặp thường xuyên khi làm việc với Docker.

    Docker image:

    Docker image giống như các blueprints, chúng là cá immutable master image được sử dụng để tạo ra các containers hoàn toàn giống nhau.

    Một docker image chứa Dockerfile, các libraries, và mã nguồn ứng dụng bạn cần chạy, tất cả chúng được bundled lại với nhau.

    Dockerfile:

    Dockerfile là một tệp chứa các lệnh chỉ dẫn cách Docker nên build image của bạn.

    Dockerfile đề cập đến một base image, đươc sử dụng để xây dựng initial image layer. Các base images phổ biến như python, ubuntu, redis…

    Các layers bổ sung sau đó có thể được xếp chồng lên các base image layers, theo các lệnh chỉ dẫn trong Dockerfile. Ví dụ, một Dockerfile cho một ứng dụng học máy có thể yêu cầu Docker thêm Numpy, Pandas, và Scikit-learn trong một layer trung gian.

    Cuối cùng, một thin layer có thể được xếp chồng lên trên các layers khác theo Dockerfile code.

    Container Registry:

    Nếu bạn muốn người khác có thể tạo các containers từ image của bạn tạo ra, bạn cần gửi image đó tới một container registry. Docker hub là registry lớn nhất và là mặc định.

    Một phép ẩn dụ

    Để khép lại phần 1, tôi đưa ra một phép ẩn dụ để minh họa các khái niệm vừa nói ở trên, bằng cách sử dụng hoạt động Nấu ăn, cụ thể là việc làm một chiếc bánh Pizza.

    Khi nấu một món ăn, ta cần có công thức cho món ăn đó, công thức giống như một Dockerfile. Nó cho bạn biết cần phải làm gì để đạt được mục tiêu. Các thành phần của món ăn giống như các layersa, bạn đã có vỏ bánh, nước sốt và pho-mát cho chiếc bánh pizza này.

    Hãy nghĩ về công thức và các thành phần được kết hợp lại thành một công cụ làm bánh pizza. Nó giống như Docker image.

    Công thức (Dockerfile) cho chúng ta biết những gì chúng ta sẽ làm. Dưới đây là kế hoạch:

    • Lớp vỏ được định dạng sẵn và immutable. Nó giống như một basic ubuntu parent image. Đây là bottom layer và được xây dựng đầu tiên.

    • Sau đó bạn sẽ thêm một ít pho mát. Thêm second layer này vào bánh pizza giống như cài đặt thêm một thư viện bên ngoài – ví dụ Numpy.

    • Sau đó bạn rắc thêm một ít húng quế. Nó giống như mã trong một tệp bạn viết để chạy ứng dụng.

    Được rồi, chúng ta hãy bắt đầu nấu ăn:

    • Lò nướng bánh pizza giống như Docker platform. Bạn đã lắp đặt lò nướng vào nhà khi chuyển đến để có thể chế biến mọi thứ với nó. Tương tự như vậy, bạn đã cài đặt Docker vào máy tính của mình để có thể "nấu" các containers.

    • Bạn khởi động lò nướng bằng cách xoay núm. Lệnh Docker run image_name giống như núm của bạn – nó tạo và khởi động container của bạn.

    • Bánh pizza đã chín giống như một Docker container đang chạy.

    • Ăn pizza giống như việc sử dụng ứng dụng.

    Giống như làm bánh pizza, tạo một ứng dụng trong một Docker container mất một số công việc, nhưng cuối cùng bạn đã có một thứ tuyệt vời!

    Lời kết: Bài viết đến đây cũng khá dài. Trong Phần 2, tôi sẽ đi sâu vào kiến trúc, các thành phần của Docker, và không phải chờ đợi lâu, ngay trong phần 2 tôi cũng sẽ giúp bạn đọc bắt đầu viết các lệnh đầu tiên với Docker.

    Tài liệu tham khảo

    [1]: James Turnbull, The Docker Book (2018)

    [2]: Preethi Kasireddy, A Beginner-Friendly Introduction to Containers, VMs and Docker (2016)

    [3]: Jeff Hale, Learn Enough Docker to be Useful (2019)

    [4]: Mumshad Mannambeth, Docker for the Absolute Beginner – Hands On – DevOps, Section 1 – Docker overview.

    Author

    Hà Hữu Linh

    [email protected]

  • Vision:  Person Segmentation

    Vision: Person Segmentation

    Overview

    Vision framework được giới thiệu lần đầu vào WWDC 2017. Vision giúp thực hiện phát hiện khuôn mặt, phát hiện văn bản, nhận dạng mã vạch, đăng ký hình ảnh… Vision cũng cho phép sử dụng Core ML để tùy chỉnh cho các tác vụ như phân loại hoặc phát hiện đối tượng.

    Trong sample này chúng ta sẽ cùng tìm hiểu Person Segmentation API, giúp ứng dụng của bạn tách mọi người trong hình ảnh khỏi môi trường xung quanh.

    alt text

    Notes: Sample yêu cầu chạy Physical device với ios 15+ hoặc chạy Simulator ios 15+ trên macOS chip Intel

    Cách tạo một Vision requests

    Muốn sử dụng bất kỳ thuật toán nào của Vision, bạn sẽ cần ba bước:

    1. Request: Yêu cầu xác định loại thứ bạn muốn phát hiện và một trình xử lý hoàn thành sẽ xử lý kết quả. Đây là subclass của VNRequest.

    2. Request handler: Sử dụng VNImageRequestHandler để thực hiện Request.

    3. Results: Kết quả sẽ được đính kèm với Request ban đầu và được chuyển đến trình xử lý hoàn thành được xác định khi tạo Request. Chúng là các subclass của VNObservation.

    Prepare the Requests

    // 1. Request
    let request = VNGeneratePersonSegmentationRequest { (request, error) in
        // 3. Results
        self.personSegmentation(request: request, error: error)
    }
            
    request.qualityLevel = .accurate
    request.outputPixelFormat = kCVPixelFormatType_OneComponent8
            
    // 2. Request Handler
    let imageRequestHandle = VNImageRequestHandler(cgImage: cgImage, options: [:])
    

    Chúng ta sẽ tạo 1 request với VNGeneratePersonSegmentationRequest. VNGeneratePersonSegmentationRequest giúp tạo mask hình ảnh cho người mà nó phát hiện trong hình ảnh. Việc set thuộc tính qualityLevel cho request thành .fast, .balanced, hoặc .accurate giúp xác định chất lượng của mask được tạo như trong hình minh họa bên dưới.

    alt text

    Việc tăng độ chính xác của mask cũng đồng hời làm giảm suất làm việc của app nên chúng ta cần lựa chọn qualityLevel phù hợp cho từng tác vụ mà nó xử lý

    alt text

    Thuộc tính tiếp theo là format đầu ra của mask: outputPixelFormat. outputPixelFormat định dạng mà mặt nạ kết quả sẽ được trả về, có 3 định dạng có thể chọn đó là:

    alt text

    Trong sample chúng ta set outputPixelFormat với kCVPixelFormatType_OneComponent8 với range từ 0 đến 255.

    Bước cuối cùng là thực hiện request với VNImageRequestHandler đã tạo từ trước. result trả về của request handle sẽ là 1 instance của VNPixelBufferObservation

    DispatchQueue.global(qos: .userInitiated).async {
        do {
            try imageRequestHandle.perform([request])
        } catch let error {
            print(error)
        }
    }
    

    Xử lý Results

    alt text

    Chúng ta đã tìm hiểu qua về request và các thuộc tính của nó, ta sẽ đến với phần tiếp theo là xử lý result trả về. Những gì ta cần làm ở đây là thay thế background của ảnh gốc nằm ngoài mask trả về từ result.

    guard let result = request.results?.first as? VNPixelBufferObservation else {
        loadingView.stopAnimating()
        return
    }
    // 1. Processing
    let buffer: CVPixelBuffer = result.pixelBuffer
    let maskImage: CIImage = CIImage(cvImageBuffer: buffer)
    let bgImage = UIImage(named: "background")!
    let background = CIImage(cgImage: bgImage.cgImage!)
    let input = UIImage(named: "humanFace")!
    let inputImage = CIImage(cgImage: input.cgImage!)
            
    // 2. Scale mask, and background to size of original image
    let maskScaleX = inputImage.extent.width / maskImage.extent.width
    let maskScaleY = inputImage.extent.height / maskImage.extent.height
    let maskScaled =  maskImage.transformed(by: __CGAffineTransformMake(maskScaleX, 0, 0, maskScaleY, 0, 0))
            
    let backgroundScaleX = inputImage.extent.width / background.extent.width
    let backgroundScaleY = inputImage.extent.height / background.extent.height
    let backgroundScaled =  background.transformed(by: __CGAffineTransformMake(backgroundScaleX, 0, 0, backgroundScaleY, 0, 0))
            
    // 3. Blending Image
    let blendFilter = CIFilter.blendWithMask()
    blendFilter.inputImage = inputImage
    blendFilter.maskImage = maskScaled
    blendFilter.backgroundImage = backgroundScaled
    
    // 4. Handle Result
    if let blendedImage = blendFilter.outputImage {
        let context = CIContext(options: nil)
        let maskDisplayRef = context.createCGImage(maskScaled, from: maskScaled.extent)
        let filteredImageRef = context.createCGImage(blendedImage, from: blendedImage.extent)
        DispatchQueue.main.async {
            self.imageDisplay.image = UIImage(cgImage: filteredImageRef!)
            self.maskImage.image = UIImage(cgImage: maskDisplayRef!)
        }
    }
    

    Đây là những gì ta làm trong đoạn code trên:

    1. Import ảnh gốc, hình nền cần thay thế và mask từ result trả về.

    2. Scale kích thước của mask và hình nền về size của ảnh gốc.

    3. Tạo ra 1 CoreImage blend filter, ta dùng blendWithRedMask() vì khi tạo ra CIImage của mask từ CVPixelBuffer của nó, nó sẽ tạo ra 1 object mặc định ở red chanel.

    4. Xử lý result filter, update UI.

    Đây là kết quả sau khi đã xử lý xong:

    alt text

    Kết luận

    Qua bài viết này mình muốn chia sẻ tới mọi người trình tự tạo một Vision requests, hiểu hơn về Person Segmentation API và cách sử dụng của nó. Mong rằng bài viết tới mình có thể chia sẻ tới các bạn cách sử dụng của Person Segmentation API với video.

    Refer

  • Triển khai CD cho dự án phát triển Website với Gitlab-CI và AWS S3

    Triển khai CD cho dự án phát triển Website với Gitlab-CI và AWS S3

    Article overview

    Giả sử chúng ta phát triển một sản phẩm Website với ReactJS và sử dụng Static Website Hosting của AWS S3. Mỗi lần deploy đều cần thực hiện build source và upload manual.
    Mục tiêu là triển khai CD để thay thế cho các công việc manual không cần thiết và giảm thiểu các sai sót ngoài ý muốn.

    Tổng quan về các công nghệ sử dụng:

    • ReactJS Website
    • Gitlab-CI
    • AWS S3, AWS CLI
    • Môi trường MacOS

    Table of contents

    Chúng ta cần một số bước sau:

    • Liên kết và khởi tạo Runner với Gitlab repository
    • [Cài đặt và cấu hình môi trường tại thiết bị chạy service runner](## Cài đặt và cấu hình môi trường tại thiết bị chạy service runner)
    • [Cấu hình các job CI/CD với .gitlab-ci.xml và Gitlab-CI](## Cấu hình các job CI/CD với gitlab-ci.xml và Gitlab-CI)

    Cài đặt và cấu hình môi trường tại thiết bị chạy service runner

    Giả định ở thiết bị MacOS chạy service runner đã build được Website ReactJS, chúng ta sẽ skip qua phần cài đặt cho ReactJS. Đầu tiên, chúng ta cần cài đặt AWS CLI.
    Sau khi cài đặt xong, ta thực hiện config với thông tin của AWS User có quyền deploy lên AWS S3 với câu lệnh sau:

    $ aws configure
    AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
    AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
    Default region name [None]: us-west-2
    Default output format [None]: json

    Cấu hình các job CI/CD với gitlab-ci.xml và Gitlab-CI

    Đầu tiên, thay vì upload bằng tay các file trong thư mục build/ chúng ta sẽ sử dụng command aws s3 sync như sau:
    aws s3 sync build/ s3://ww95 với ww95 là tên của Bucket đang Host Website.
    Sau đó, chúng ta sẽ cài đặt command package.json như sau:

      "scripts": {
        "build": "react-scripts build", // Build source code để deploy
        "deploy": "aws s3 sync build/ s3://ww95" // Thực hiện deploy lên S3
      },

    Tiếp đó, ta sẽ cấu hình .gitlab-ci.yml để hệ thống tự động deploy khi có thay đổi trên nhánh master.

    stages:
      - Deployment
    deploy:
      stage: Deployment
      before_script: []
      only:
          - master
      allow_failure: true
      script:
        - yarn install
        - yarn build
        - yarn deploy

    Tuy nhiên, khi chạy thực tế trên Gitlab-CI ta sẽ gặp lỗi sau:

    Treating warnings as errors because process.env.CI = true.
    Most CI servers set it automatically. 
    Failed to compile.

    Để giải quyết vấn đề này ta phải setting lại process.env.CI = false bằng hai cách sau:

    • Thay đổi cấu hình .gitlab-ci.yml từ yarn build -> CI=false yarn build.
    • Cài đặt biến môi trường trong Gitlab-CI như ảnh sau

    Sau đó merge code vào master, và hưởng thành quả. Từ giờ các bạn không cần phải deploy bằng tay nữa rồi, chúc các bạn may mắn.

    Authors

    MinhNN44

  • Async await trong swift – Part 1

    Async await trong swift – Part 1

    Apple đã giới thiệu cho cộng đồng dev iOS về Swift 5.5, trong đó có sự cập nhật rất là lớn. Đó là về bất đồng bộ, với async await

    Để xem async / await giúp ngôn ngữ như thế nào, sẽ hữu ích khi xem cách chúng tôi đã giải quyết vấn đề tương tự trước đây.

    Một ví dụ về networking trước khi async await được update

    class SeverConnection {
        let defaultSession = URLSession(configuration: URLSessionConfiguration.default)
        var dataTask: URLSessionDataTask?
        var downloadTask: URLSessionDownloadTask?
        var uploadTask: URLSessionUploadTask?
    }
    
    extension SeverConnection {
        func fetchAPIFromURL(_ url: String, completion: @escaping (String?, Error?) -> Void) {
            guard let url = URL(string: url) else {
                completion(nil, SeverConnectionError.badURL)
                return
            }
            dataTask = defaultSession.dataTask(with: url, completionHandler: { (data, response, error) in
                if let error = error {
                    completion(nil, SeverConnectionError.errorWithDataTask)
                    return
                }
                guard let httpResponse = response as? HTTPURLResponse,
                    (200...299).contains(httpResponse.statusCode) else {
                        completion(nil, SeverConnectionError.badResponse)
                        return
                }
                guard let data = data else { return }
                let dataString = String(data: data, encoding: .utf8)
                completion(dataString, nil)
            })
            dataTask?.resume()
        }
    }
    

    khi sử dụng:

    private func fetchListData() {
            let urlString = "https://vapor-mock.herokuapp.com/pic_dic.json"
            severConnection.fetchAPIFromURL(urlString) { [weak self] (data, error) in
                guard let self = self else {
                    return
                }
                if let error = error {
                    print(error)
                }
                if let data = data {
                    self.convertData(data)
                }
            }
        }
        
        private func convertData(_ data: String) {
            let responseData = Data(data.utf8)
            let decoder = JSONDecoder()
            
            var responseImageData: [ImageData]?
            
            do {
                responseImageData = try decoder.decode([ImageData].self, from: responseData)
                infoImages = responseImageData
                DispatchQueue.main.async {
                    self.mainTableView.reloadData()
                }
            } catch {
                print("Error decoding imageData: \(error)")
            }
        }
    

    Còn với async await

    class FetchAPITask {
        static let shared = FetchAPITask()
        
        func fetchAPI<D: Decodable>(url: URL) async throws -> D {
            let task = Task { () -> D in
                try await fetchAndDecode(url: url)
            }
            return try await task.value
        }
        
        func fetchAPIGroup<D: Decodable>(urls: [URL]) async throws -> [D] {
            try await withThrowingTaskGroup(of: D.self) { (group)  in
                for url in urls {
                    group.async { try await self.fetchAndDecode(url: url) }
                }
                var results = [D]()
                for try await result in group {
                    results.append(result)
                }
                return results
            }
        }
        
        func fetchAndDecode<D: Decodable>(url: URL) async throws -> D {
            let data = try await URLSession.shared.fetchData(with: url)
            let decodedData = try JSONDecoder().decode(D.self, from: data)
            return decodedData
        }
    }
    

    khi sử dụng:

    func fetchAllAPI() async {
        do {
            if let url = url {
                let datas: [ImageData] = try await FetchAPITask.shared.fetchAPI(url: url)
                self.datas = datas
            }
        } catch {
            print(error)
        }
    }
    
    Task.init(priority: .default, operation: {
        await self.fetchAllAPI()
        DispatchQueue.main.async {
            self.mainTableView.reloadData()
        }
    })
    

    Kết luận

    • Sử dụng async await với syntax đơn giản hơn dễ đọc hơn giúp việc đọc hiểu và maintain sẽ dễ dàng hơn
    • Các vấn đề với closure (điển hình như việc nhiều callback lồng nhau)
    • Đây là một ví dụ về việc sử dụng async await call api async await còn rất nhiều thứ hay ho mọi người có thể đọc tại đây
  • Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 1)

    Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 1)

    Table of contents

    • Đặt tên biến
    • Đặt tên hàm
    • Đặt tên class, struct, enum, protocol
    • Spacing
    • Comment
    • Access Control
    • Self & Closure

    Đặt tên biến

    • Hai quy tắc cơ bản nhất khi đặt tên biến đó là: sử dụng tiếng Anh thay vì tiếng Việt, sử dụng lowerCamelCase (kiểu lạc đà) thay vì snake_case

    Not Preferred

     private let height_normal_avatar: CGFloat = 60.0
     private let chieuRongNormalAvatar: CGFloat = 120.0
    

    Preferred

     private let heightNormalAvatar: CGFloat = 60.0
     private let widthNormalAvatar: CGFloat = 120.0
    
    • Khi đặt tên biến, hãy chú trọng đến sự rõ ràng, rành mạch hơn là sự ngắn gọn. Cố gắng làm sao khi đọc tên biến lên, ta có thể tưởng tượng được ngay biến đó có nhiệm vụ gì hoặc đang ám chỉ đến đối tượng nào. Vì vậy khi đặt tên biến không nên viết tắt và cũng không nên đặt tên giống với các đối tượng của hệ thống

    Not Preferred

    @IBOutlet private weak var tableView: UITableView!
    @IBOutlet private weak var imgAvatar: UIImageView!
    @IBOutlet private weak var lblbName: UILabel!
    
    private var bool: Bool = false
    

    Preferred

    @IBOutlet private weak var salaryTableView: UITableView!
    @IBOutlet private weak var avatarImageView: UIImageView!
    @IBOutlet private weak var nameLabel: UILabel!
    
    private var isLoadingState: Bool = false
    
    • Tên biến nên được bắt đầu bằng 1 danh từ và khi khai báo biến, nên khai báo luôn kiểu dữ liệu của biến đó (điều này có thể làm giảm được phần nào thời gian compile của app)

    Not Preferred

    private var dataSalaryArray = [Salary]()
    private var isLoadingState = false
    

    Preferred

    private var dataSalaryArray: [Salary] = [Salary]()
    private var isLoadingState: Bool = false
    
    • Với những biến cùng kiểu, nên đặt tên có sự thống nhất từ trên xuống dưới, tránh tình trạng mỗi biến 1 style đặt tên khác nhau. Ví dụ trong trường hợp với height và top constraint của đối tượng avatarImageView

    Not Preferred

    @IBOutlet private weak var heightOfAvatarImageView: NSLayoutConstraint!
    @IBOutlet private weak var topConstraintForAvatarImageView: NSLayoutConstraint!
    

    Preferred

    @IBOutlet private weak var heightConstraintAvatarImageView: NSLayoutConstraint!
    @IBOutlet private weak var topConstraintAvatarImageView: NSLayoutConstraint!
    

    Đặt tên hàm

    • Cũng giống như việc đặt tên biến, việc đặt tên hàm cũng có các quy tắc tương tự: dùng tiếng Anh, dùng kiểu lowerCamelCase (kiểu lạc đà)
    • Tên hàm thường được bắt đầu bằng động từ, tên hàm phải rõ ràng, rành mạch. Cố gắng làm sao khi đọc tên hàm lên, ta có thể tưởng tượng được ngay hàm đó làm nhiệm vụ gì.
    • Đối với những function có nhiều param, nên đặt mỗi param trên 1 dòng và căn lề cho chúng. Ngoài ra, với những param có giá trị mặc định, nên đặt chúng ở cuối list parameter. Còn những param không có giá trị mặc định thì nên đặt lên đầu

    Not Preferred

    func setAttributedString(string: String, font: UIFont, lineSpacing: CGFloat, alignment: NSTextAlignment = .left, icon: UIImage? = nil, íconRect: CGRect? = nil) -> NSAttributedString {
        // Do something
    }
    

    Preferred

    func setAttributedString(string: String, 
                            font: UIFont, 
                            lineSpacing: CGFloat, 
                            lignment: NSTextAlignment = .left, 
                            icon: UIImage? = nil, 
                            íconRect: CGRect? = nil) -> NSAttributedString {
        // Do something
    }
    
    • Bên cạnh việc sử dụng parameter 1 cách truyền thống, ta có thể sử dụng thêm specifying argument labels (thêm 1 label vào trước tên của param) hoặc omitting argument labels (thêm dấu gạch dưới _ vào trước tên của param). Điều này làm cho việc gọi tên hàm sẽ trở nên gần gũi hơn với ngôn ngữ tự nhiên. (Tham khảo thêm tại Function)

    Preferred

    func greet(person: String, from hometown: String) -> String {
        return "Hello \(person)!  Glad you could visit from \(hometown)."
    }
    
    print(greet(person: "Bill", from: "Cupertino"))
    // Prints "Hello Bill!  Glad you could visit from Cupertino."
    

    Preferred

    func welcome(_ person: String, from hometown: String) -> String {
        return "Hello \(person)!  Glad you could visit from \(hometown)."
    }
    
    print(welcome("Bill", from: "Cupertino"))
    // Prints "Hello Bill!  Glad you could visit from Cupertino."
    

    Đặt tên class, struct, enum, protocol

    • Đặt tên class, struct, enum và protocol ta cũng sử dụng tiếng Anh nhưng sẽ dùng kiểu UpperCamelCase – đây là điểm khác biệt so với function và property
    • Tên class, struct, enum, protocol thường được bắt đầu bằng danh từ và khi đặt tên cũng cần ưu tiên sự rõ ràng, rành mạch.

    Spacing

    • Các dấu ngoặc nhọn mở đầu cho các function và các dấu ngoặc sau các biểu thức if/else/switch/while… đều phải được mở trên cùng 1 dòng với câu lệnh, có thêm 1 khoảng trắng phía bên trái và đóng trên 1 dòng khác

    Not Preferred

    if user.isHappy
    {
      // Do something
    }
    else {
      // Do something else
    }
    

    Preferred

    if user.isHappy {
      // Do something
    } else {
      // Do something else
    }
    
    • Nên có 1 dòng trắng giữa các function, giữa các block code và giữa khu vực khai báo các properties với khu vực ánh xạ các outlet. Trong 1 function cũng cần có những dòng trắng để phân tách các chức năng nhỏ trong function đó.

    Not Preferred

    import UIKit
    class SettingScreen: UIViewController {
        @IBOutlet private weak var settingTitleLabel: UILabel!
        @IBOutlet private weak var stateSettingSwitch: UISwitch!
        var snapView: UIView?
        var snapTabbarView: UIView?
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
        }
    }
    extension SettingScreen {
        func loadData(state: String) {
            DispatchQueue.main.async {
                self.settingTitleLabel.text = state
            }
        }
    }
    

    Preferred

    import UIKit
    
    class SettingScreen: UIViewController {
    
        @IBOutlet private weak var settingTitleLabel: UILabel!
        @IBOutlet private weak var stateSettingSwitch: UISwitch!
    
        var snapView: UIView?
        var snapTabbarView: UIView?
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
        }
    }
    
    extension SettingScreen {
    
        func loadData(state: String) {
            DispatchQueue.main.async {
                self.settingTitleLabel.text = state
            }
        }
    }
    
    • Dấu 2 chấm luôn không có khoảng trắng ở phía bên trái và có 1 khoảng trắng ở phía bên phải. Ngoại trừ 3 trường hợp: toán tử 3 ngôi A ? B : C, empty dictionary [:] và #selector syntax addTarget(_:action:)

    Not Preferred

     class ViewController : UIViewController {
         private var data :[String:CGFloat] = ["A" : 1.2, "B":3.2]
     }
    

    Preferred

     class ViewController: UIViewController {
         private var data: [String: CGFloat] = ["A": 1.2, "B": 3.2]
     }
    
    • Không nên có khoảng trắng ở cuối mỗi dòng code nhưng nên có thêm 1 dòng trắng ở cuối mỗi file
    • Không có giới hạn nhất định cho số ký tự trên mỗi dòng code. Tuy nhiên mỗi dòng code của bạn không nên có quá 100 ký tự. Có 1 vài cách để làm giảm số lượng ký tự trên 1 dòng code:
      • Đối với function có nhiều param và các param có tên quá dài, bạn có thể xuống dòng và căn lề cho chúng (đã để cập ở trên)
      • Đối với các biểu thức tính toán, bạn có thể đặt ra các biến phụ thay vì gộp chung lại vào 1 biểu thức

    Not Preferred

    // Tính diện tích tam giác bất kỳ khi biết độ dài 3 cạnh (hệ thức Heron)
    func calculateSquareTriangleUsingHeron(firstEdge: CGFloat,
                                           secondEdge: CGFloat,
                                           thirdEdge: CGFloat) -> CGFloat {
        return sqrt(((firstEdge + secondEdge + thirdEdge) / 2) * ((firstEdge + secondEdge + thirdEdge) / 2 - firstEdge) * ((firstEdge + secondEdge + thirdEdge) / 2 - secondEdge) * ((firstEdge + secondEdge + thirdEdge) / 2 - thirdEdge))
    }
    

    Preferred

    // Tính diện tích tam giác bất kỳ khi biết độ dài 3 cạnh (hệ thức Heron)
    func calculateSquareTriangleUsingHeron(firstEdge: CGFloat,
                                           secondEdge: CGFloat,
                                           thirdEdge: CGFloat) -> CGFloat {
        let halfPerimeter: CGFloat = (firstEdge + secondEdge + thirdEdge) / 2
        let halfPerimeterMinusA: CGFloat = halfPerimeter - firstEdge
        let halfPerimeterMinusB: CGFloat = halfPerimeter - secondEdge
        let halfPerimeterMinusC: CGFloat = halfPerimeter - thirdEdge
        let doubleSquare: CGFloat = halfPerimeter * halfPerimeterMinusA * halfPerimeterMinusB * halfPerimeterMinusC
        let square: CGFloat = sqrt(doubleSquare)
        return square
    }
    

    Comment

    • Đôi khi ta cần phải add comment để chú thích cho các đoạn code, phục vụ cho quá trình maintenance sau này. Tất nhiên là khi code được thay đổi thì comment cũng cần được update theo.
    • Khi comment, không nên dùng C-style /*…*/ mà nên dùng double-slash // hoặc triple-slash ///. Cũng không nên để code và comment xuất hiện trên cùng 1 dòng

    Access Control

    • Các function và property nên mặc định để là private hoặc fileprivate để đảm bảo tính đóng gói trong lập trình. Nên hạn chế việc sử dụng open, public hoặc internal. Tham khảo thêm tại Access Control

    Not Preferred

    class SalaryCell: UITableViewCell {
        @IBOutlet weak var monthLabel: UILabel!
        @IBOutlet weak var incomeLabel: UILabel!
        
        override func awakeFromNib() {
            super.awakeFromNib()
        }
    }
    
    extension ViewController: UITableViewDataSource {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return dataSalaryArray.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                for: indexPath) as? SalaryCell else {
                return UITableViewCell()
            }
            cell.monthLabel.text = dataSalaryArray[indexPath.row].month
            cell.incomeLabel.text = dataSalaryArray[indexPath.row].incomeLabel
            return cell
        }
    }
    

    Preferred

    class SalaryCell: UITableViewCell {
        @IBOutlet private weak var monthLabel: UILabel!
        @IBOutlet private weak var incomeLabel: UILabel!
        
        override func awakeFromNib() {
            super.awakeFromNib()
        }
        
        func setupData(data: Salary) {
            monthLabel.text = data.month
            incomeLabel.text = "\(data.income)"
        }
    }
    
    extension ViewController: UITableViewDataSource {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return dataSalaryArray.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                for: indexPath) as? SalaryCell else {
                return UITableViewCell()
            }
            cell.setupData(data: dataSalaryArray[indexPath.row])
            return cell
        }
    }
    
    • Khi khai báo property, các từ khoá liên quan đến access control nên được đặt lên đầu. Chỉ có 1 số từ khoá được đứng trước chúng đó là: static, @IBAction, @IBOutlet, @discardableResult.

    Not Preferred

    @IBOutlet weak private var salaryTableView: UITableView!
    @IBOutlet weak private var fullNameLabel: UILabel!
    

    Preferred

    @IBOutlet private weak var salaryTableView: UITableView!
    @IBOutlet private weak var fullNameLabel: UILabel!
    

    Self & Closure

    • Không nên sử dụng từ khoá self một cách tuỳ ý. Chỉ dùng self trong 2 trường hợp sau:
      • Khi trình biên dịch yêu cầu, thường là khi đang trong biểu thức closure
      func loadData() {
          DispatchQueue.main.async {
              self.salaryTableView.reloadData()
          }
      }
      
      • Khi đang ở trong hàm init, ta cần phân biệt giữa property của object và param của hàm init
      class Salary {
          var income: Int
          var month: String
      
          init(income: Int, month: String) {
              self.income = income
              self.month = month
          }
      }
      
    • Đối với closure, ta có thể dùng trailling closure syntax trong trường hợp chỉ có duy nhất 1 biều thức closure trong list parameter. Còn nếu có nhiều hơn 1, ta phải giữ lại tên cho các closure đó.

    Not Preferred

    // Trong trường hợp này, chỉ có duy nhất 1 biểu thức closure nên không cần thiết phải để lại label "animations"
    UIView.animate(withDuration: 1, animations: {
        self.avatarImageView.alpha = 0.0
    })
            
    // Trường hợp này có 2 biểu thức closure, vì vậy nên để lại cả 2 label "animations" và "completion" để phân biệt chúng với nhau
    UIView.animate(withDuration: 1) {
        self.avatarImageView.alpha = 0.0
    } completion: { (_) in
        self.avatarImageView.removeFromSuperview()
    }
    

    Preferred

    UIView.animate(withDuration: 1) {
        self.avatarImageView.alpha = 0.0
    }
            
    UIView.animate(withDuration: 1,
                    animations: {
                        self.avatarImageView.alpha = 0.0
                    }, completion: { _ in
                        self.avatarImageView.removeFromSuperview()
                    })
    

    To be continue…

  • Transfer file lên AWS EC2 với SFTP

    Transfer file lên AWS EC2 với SFTP

    Giả sử chúng ta có một AWS EC2 instance sử dụng linux và cần upload/download file. Trong trường hợp này, chúng ta có thể sử dụng SFTP để thực hiện upload/download file lên server.

    Mặc định, chúng ta cần dùng file key “.pem” để authen cho user ec2-user khi SSH vào EC2 instance. Chúng ta có thể sử dụng file .pem này để thực hiện SFTP tới instance như sau.

    Chú ý: Phần Host chúng ta điền public IP của instance.

    Tuy nhiên, phương pháp này có một số hạn chế và nguy cơ:

    1. Cần có file .pem để có thể SFTP tới EC2 instance. Rất bất tiện và yêu cầu phải chia sẻ file .pem nếu thực hiện SFTP trên nhiều thiết bị khác nhau.
    2. Việc chia sẻ và sử dụng file .pem để SFTP rất không an toàn. Nếu file .pem rơi vào tay kẻ xấu, họ có thể truy cập vào EC2 instance (SSH) và lấy cắp nhiều thông tin khi ec2-user có thể switch sang account root.

    Để giải quyết vấn đề này, chúng ta cần thực hiện chuyển cơ chế SFTP từ key .pem sang username/password và phân quyền cho các user đó.

    Chú ý, các câu lệnh sau cần được thực thi với quyền root

    Đầu tiên, chúng ta tạo ra các user để dành riêng cho việc sử dụng SFTP thay vì ec2-user:

    adduser user_gsthl
    adduser user_gstdn
    adduser user_gsthcm

    Và cài đặt mật khẩu cho các user với câu lệnh sau:

    passwd user_gsthl
    passwd user_gstdn
    passwd user_gsthcm

    Tiếp đó, chúng ta cần tạo một group dành riêng cho các user có quyền được phép sử dụng SFTP đến EC2 instance và add các user đó vào group:

    groupadd sftp
    usermod -a -G sftp user_gsthl
    usermod -a -G sftp user_gstdn
    usermod -a -G sftp user_gsthcm

    Chúng ta có thể kiểm tra các user ở trong group với câu lệnh sau:
    grep sftp /etc/group

    Tiếp đó, chúng ta tạo một thư mục dành riêng cho việc lưu trữ các file chuyển qua SFTP và phân quyền cho thư mục đó.
    mkdir -p /public/sftp/
    chmod 755 /public/sftp/
    Tiếp đó, ta tạo một file trong thư mục để client có thể download về.

    touch /public/sftp/hello.txt
    echo "This is a hello from SFTP directory" > /public/sftp/hello.txt

    Config file SFTP bằng cách thực hiện command sau
    sudo nano /etc/ssh/sshd_config
    Thêm đoạn config sau vào cuối của file sshd_config

    Port 22
    Subsystem sftp internal-sftp
    Match Group sftp
    ChrootDirectory /public/sftp
    X11Forwarding no
    AllowTcpForwarding no
    ForceCommand internal-sftp
    PasswordAuthentication yes

    Sau khi config, chúng ta sử dụng lệnh sudo systemctl restart sshd để khởi động lại sshd service. Nếu có lỗi trong quá trình khởi động lại, sử dụng lệnh systemctl status sshd.service -l để thực hiện kiểm tra trạng thái của service.

    Sau đó, ta có thể sử dụng FileZilla để kiểm tra SFTP đến server như sau:

    Như vậy là ta đã cài đặt xong SFTP sử dụng username/password cho một group user cho AWS EC2 chạy Linux. Mong là bài viết có thể giúp ích cho các bạn giải quyết các vấn đề liên quan đến SFTP với AWS EC2. Nếu có câu hỏi hay góp ý nào, rất mong mọi người comment, mình sẽ giải đáp và tiếp thu.

    Authors

    TinNH6

  • How To Create A Framework In Swift

    How To Create A Framework In Swift

    Updated for Swift 5

    Vì sao nên sử dụng framework

    • Việc sử dụng các tính chất hướng đối tượng trong lập trình rất phổ biến trong đó có tính chất kế thừa
    • Nhưng với việc swiftUI được ra đời và với SwiftUI thì View nó là struct, nên không thể kế thừa
    • Vậy có cách nào để thay thế được kế thừa trong swift?
    • Đối với riêng SwiftUI nói riêng hay việc sử dụng các framework iOS nói chung: Việc sử dụng tính chất kế thừa sẽ có những issue như swiftUI đang sử dụng View là struct ( struct k có các tính chất hướng đối tượng như class). Vâyviệc Customize 1 class để tái sử dụng như Class MainView: CustomizeView hay các phương thức như override hay class cha có các property gì thì class con cũng sẽ có các property đấy

    Tạo framework như thế nào?

    • Từ Xcode 11, Apple đã công bố tạo một framework được gọi là XCFramework
      • Mở Xcode, File > New > Project và chọn Framework

    • Triển khai mã code cho framwork tại đây
    • Tiếp theo sử dụng xcodebuild archive để tạo kho lưu trữ

      xcodebuild archive -scheme PROJECTNAME_HERE -destination=”iOS” -archivePath /tmp/xcf/ios.xcarchive -derivedDataPath /tmp/iphoneos -sdk iphoneos SKIP_INSTALL=NO BUILD_LIBRARIES_FOR_DISTRIBUTION=YES

      xcodebuild archive -scheme PROJECTNAME_HERE -destination=”iOS Simulator” -archivePath /tmp/xcf/iossimulator.xcarchive -derivedDataPath /tmp/iphoneos -sdk iphonesimulator SKIP_INSTALL=NO BUILD_LIBRARIES_FOR_DISTRIBUTION=YES destination: tùy chọn xác định nền tảng mục tiêu và các thiết bị.archivePath: đường dẫn tới thư mục cần tạo framework

    • Sau khi chạy 2 lệnh ở trên thành công
    • Tiếp theo sử dung câu lệnh này để đóng gói framwork

      xcodebuild -create-xcframework -framework /tmp/xcf/ios.xcarchive/Products/Library/Frameworks/PROJECTNAME_HERE.framework -framework /tmp/xcf/iossimulator.xcarchive/Products/Library/Frameworks/PROJECTNAME_HERE.framework -output FRAMEWORK_NAME.xcframework

      File đã được gen ra thành công:

    • Add framework đã tạo vào trong project cần dùng và để sang Embed & Sign 
    • Import framework vào class cần sử dụng

    (more…)