Category: Tip

  • Thread.sleep() and kotlinx.coroutines.delay()

    Thread.sleep() and kotlinx.coroutines.delay()

    Chào mọi người, trong lập trình Android, khi gặp một yêu cầu liên quan đến delay hoặc dừng một task vụ sau một khoảng thời gian chờ nhất định, chúng ta thường nghĩ đến 2 phương án là Thread.sleep() và kotlinx.coroutines.delay().

    Vậy 2 cơ chế này có gì khác nhau và khi nào mình nên sử dụng chúng. Hôm nay mình sẽ xin chia sẻ tới mọi người ý hiểu của mình.

    1. Khái niệm

    kotlinx.coroutines.delay() và Thread.sleep() đều là cơ chế giới thiệu độ trễ hoặc tạm dừng trong quá trình thực thi chương trình, nhưng chúng được sử dụng trong các ngữ cảnh khác nhau và có các đặc điểm khác nhau.

    1.1. kotlinx.coroutines.delay()

    kotlinx.coroutines.delay() là một suspending function được cung cấp bởi thư viện Kotlin coroutines. Nó thường được sử dụng trong các asynchronous programming khi làm việc với các coroutine, nơi bạn muốn tạm dừng việc thực thi một coroutine mà không chặn luồng chính.

    1.2. Thread.sleep()

    Thread.sleep() là một static method được cung cấp bởi lớp Thread trong Java (cũng có thể sử dụng được trong Kotlin) và được sử dụng để tạm dừng quá trình thực thi của luồng hiện tại.

    Thread.sleep() không chỉ tạm dừng việc thực thi một coroutine mà còn chặn toàn bộ luồng, nghĩa là các luồng khác trong ứng dụng sẽ không thể tiếp tục công việc của chúng trong suốt thời gian Sleep.

    2. Ví dụ triển khai

    2.1. kotlinx.coroutines.delay()

    fun main(args: Array<String>) {
        runBlocking {
                run()
            }
        }
    }
    
    suspend fun run() {
        coroutineScope {
            val timeInMillis = measureTimeMillis {
                val mainJob = launch {
                    //Job 0
                    launch {
                        print("A->")
                        delay(1000)
                        print("B->")
                    }
                    //Job 1
                    launch {
                        print("C->")
                        delay(2000)
                        print("D->")
                    }
                    //Job 2
                    launch {
                        print("E->")
                        delay(500)
                        print("F->")
                    }
    
                    //Main job
                    print("G->")
                    delay(1500)
                    print("H->")
                }
    
                mainJob.join()
            }
    
            val timeInSeconds =
                String.format("%.1f", timeInMillis/1000f)
            print("${timeInSeconds}s")
       }
    }
    

    Luồng chạy sẽ như nhau:

    Main Job sẽ chạy và sẽ bị tạm dừng bởi delay.

    Tiếp đó là Job 0 -> Job 1 -> Job 2, 3 jobs đều khởi động cùng một lúc và bị delay với thời gian tương ứng trong từng job.

    Tiếp theo, task có delay() time ngắn nhất thì chạy xong trước, theo sau là các công việc tiếp theo hoàn thành. Độ trễ dài nhất là 2s.

    Kết quả là:

    G->A->C->E->F->B->H->D->2.0s

    or

    G->A->C->E->F->B->H->D->2.1s

    Nhìn vào kết quả, tại sao là 2.1s mà không phải là delay() time dài nhất 2.0s. Vì các bạn có thể thấy khoảng time từ khi Main Job bắt đầu đến khi delay của Job1 là 0.01s. Có một số lần chạy, khoảng time đó quá nhỏ nên kết quả có thể là 2s.

    2.2. Thread.sleep() on Dispatchers.Main

    fun main(args: Array<String>) {
        runBlocking {
                run()
            }
        }
    }
    
    
    suspend fun run() {
        coroutineScope {
            val timeInMillis = measureTimeMillis {
                val mainJob = launch {
                    //Job 0
                    launch {
                        print("A->")
                        delay(1000)
                        print("B->")
                    }
                    //Job 1
                    launch {
                        print("C->")
                        Thread.sleep(2000)
                        print("D->")
                    }
                    //Job 2
                    launch {
                        print("E->")
                        delay(500)
                        print("F->")
                    }
    
                    //Main job
                    print("G->")
                    delay(1500)
                    print("H->")
                }
    
                mainJob.join()
            }
    
            val timeInSeconds =
                String.format("%.1f", timeInMillis/1000f)
            print("${timeInSeconds}s")
        }
    
    }
    
    

    Luồng chạy sẽ như nhau:

    Main Job sẽ chạy và sẽ bị tạm dừng bởi delay.

    Tiếp đó là Job 0 -> Job 1. Job sẽ bị suspended. Tuy nhiên Thread.sleep(2000) chạy ở Job 1, nó sẽ block Main Thread trong 2s. Job2 trong thời gian dó không thực thi gì cả.

    Sau 2s, D sễ được in ra, tiếp đó là E của Job2. Job 2 sẽ bị suspend. Vì Main Job và Job 1 có suspend time nhỏ hơn 2s, vậy nên sau đó nó sẽ được in ra ngay lập tức. Lúc này Job 0 sẽ được chạy trước vì có time delay bé hơn.

    Cuối cùng là 0.5s, Job2 sẽ tiếp tục chạy và hoàn thành in ra F.

    Kết quả là: G->A->C->D->E->B->H->F->2.5s

    Timestamp #1 (sau 0 second)

    • Main job và Job 0 start and suspended.

    • Job 1 start và blocks Thread

    Timestamp #2 (sau 2 seconds)

    • Job 1 hoàn thành

    • Job 2 start and suspended.

    • Job 0 và Main Job tiếp tục và hoàn thành.

    Timestamp #3 (after 0.5 seconds)

    • Job 3 tiếp tục và hoàn thành.

    Khoảng thời gian tổng dao động trong 2.5s.

    2.3. Thread.sleep() on Dispatchers.Default/IO or Dispatchers.Default/Default

    fun main(args: Array<String>) {
        withContext(Dispatchers.Default) {
            run()
        }
    }
    
    
    suspend fun run() {
        coroutineScope {
            val timeInMillis = measureTimeMillis {
                val mainJob = launch {
                    //Job 0
                    launch {
                        print("A->")
                        delay(1000)
                        print("B->")
                    }
                    //Job 1
                    launch {
                        print("C->")
                        Thread.sleep(2000)
                        print("D->")
                    }
                    //Job 2
                    launch {
                        print("E->")
                        delay(500)
                        print("F->")
                    }
    
                    //Main job
                    print("G->")
                    delay(1500)
                    print("H->")
                }
    
                mainJob.join()
            }
    
            val timeInSeconds =
                String.format("%.1f", timeInMillis/1000f)
            print("${timeInSeconds}s")
        }
    
    }
    
    

    Kết quả là:

    G->A->C->E->F->B->H->D->2.0s

    Kết quả khá tương tự với ví dụ sử dụng kotlinx.coroutines.delay()

    Khi Dispatchers.Default hoặc Dispatchers.IO được sử dụng, nó được hỗ trợ bởi một nhóm luồng. Mỗi lần chúng ta gọi launch{}, một worker thread khác được created/used.

    Ví dụ, đây là các Worker thread đang được sử dụng:

    Main Job – DefaultDispatcher-worker-1

    Job 0 – DefaultDispatcher-worker-2

    Job 1 – DefaultDispatcher-worker-3

    Job 2 – DefaultDispatcher-worker-4

    Để xem luồng nào hiện đang chạy, bạn có thể sử dụng println("Run ${Thread.currentThread().name}")

    Vì vậy, Thread.sleep() thực sự chặn luồng đó, nhưng chỉ chặn DefaultDispatcher-worker-3. Các công việc khác vẫn có thể được tiếp tục chạy vì chúng nằm trên các luồng khác nhau.

    Timestamp #1 (after 0 second)

    • Main Job, Job 0, Job 1 và Job 2 start.

    • Main Job, Job 0 và Job2 bị suspended.

    • Job 1 bị block trên Thread của nó, ở đây là DefaultDispatcher-worker-3.

    Timestamp #2 (after 0.5 second)

    • Job 2 tiếp tục và hoàn thành

    Timestamp #3 (after 1 second)

    • Job 0 tiếp tục và hoàn thành

    Timestamp #4 (after 1.5 seconds)

    • Main Job tiếp tục và hoàn thành

    Timestamp #5 (after 2 seconds)

    • Job 1 tiếp tục và hoàn thành

    Bởi vì mỗi công việc chạy trên một luồng khác nhau, công việc có thể được bắt đầu vào những thời điểm khác nhau. Vì vậy, đầu ra của A, C, E, G có thể là ngẫu nhiên. Như vậy, bạn thấy trình tự các task có thể khác với trình tự trong Exampe 1 ở phần trên.

    3. Conclusion

    Thread.sleep() chặn luồng gọi nó còn kotlinx.coroutines.delay() thì không.

    Chỉ nên Thread.sleep() để kiểm tra xem tôi đã đặt đúng tác vụ chạy dài vào chuỗi nền chưa.

    Cuối cùng, kotlinx.coroutines.delay() là phương pháp được khuyến nghị để tạo độ trễ trong một coroutine mà không chặn luồng giao diện người dùng.

    Cảm ơn các bạn đã theo dõi.

  • Android Room Database Tips

    Android Room Database Tips

    Chào mọi người, chắc hẳn trong chúng ta nếu triển khai database của Android trong thời điểm hiện tại, chúng ta sẽ nghĩ ngay đến việc sử dụng Room. Chính vì vậy, hôm nay mình xin chia sẻ một số Tips nhỏ trong việc sử dụng Room đến mọi người.

    1. Thiết lập ràng buộc giữa các Entities thông qua ForeignKey

    Mặc dù Room không hỗ trợ trực tiếp ràng buộc giữa các mối quan hệ, nhưng nó cho phép bạn xác định các ràng buộc Foreign keys giữa các Entities.

    Thông qua annotation @ForeignKey, một phần trong bộ annotation của @Entity, để cho phép sử dụng các tính năng khóa ngoại của SQLite. No không những giúp thể hiện được tốt hơn mối quan hệ giữa các Entities, đảm bảo đúng thiết kế mà còn thực thi các ràng buộc trên các bảng để đảm bảo mối quan hệ hợp lệ khi bạn sửa đổi cơ sở dữ liệu.

    Cụ thể chúng ta sẽ cùng đến một ví dụ cụ thể:

    image

    Cùng nhìn quan hệ 1-n giữa Person và Dog. Cụ thể với 2 Primary Key tương ứng là PersonIdDogId cùng với PersonId được sử dụng như là một foreign key.

    @Entity(tableName = “dog”,
            foreignKeys = arrayOf(
                ForeignKey(entity = Person::class,
                           parentColumns = arrayOf(“personId),
                           childColumns = arrayOf("owner"))))
    
    data class Dog(@PrimaryKey val dogId: String,
                  val name: String,
                  val owner: String)
    

    Theo tùy chọn, bạn có thể tuỳ chọn hành động sẽ được thực hiện khi đối tượng Parent Entity bị xóa hoặc cập nhật trong cơ sở dữ liệu.

    Bạn có thể chọn một trong các tùy chọn sau: NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT hoặc CASCADE, tương tự sử dụng như trong SQLite.

    2. Tạo mối quan hệ Relation trong Room Database

    Vẫn là mối quan hệ 1-n ở ví dụ trước. Bây giờ mình muốn truy vấn thực hiện việc lấy dữ liệu của các Person và toàn bộ Dogs tương ứng kèm theo.

    image

    Cách thông thường để thực hiện, chúng ta sẽ cần thực hiện 2 truy vấn: một truy vấn để lấy danh sách tất cả Person và một truy vấn khác để lấy danh sách Dog dựa trên Id của Person. Cụ thể:

    @Query(“SELECT * FROM Person”)
    public List<Person> getPersons();
    
    @Query(“SELECT * FROM dog where owner = :personId”)
    public List<Dog> getDogsForPersons(String personId);
    

    Sau đây, mình sẽ triển khai theo cách thực hiện tạo mỗi quan hệ Relation giữa 2 đối tượng Person và Dog thông qua annotation @Relation

    class PersonAndDogs {
       @Embedded
       var person: Person? = null
       @Relation(parentColumn = “personId”,
                 entityColumn = “owner”)
       var dogs: List<Dog> = arrayListOf()
    }
    

    Trong DAO, chúng tôi chỉ thực hiện một truy vấn duy nhất và Rôm sẽ truy vấn cả bảng Person và Dog, tiếp đó xử lý mapping đối tượng.

    @Transaction
    @Query(“SELECT * FROM Person”)
    List<PersonAndDogs> getPersonAnDogs();
    

    3. Thực hiện câu lệnh trong một Transaction

    Khi một câu lệnh trong Room gắn với @Transaction, nó sẽ đảm bảo rằng tất cả các hoạt động cơ sở dữ liệu mà bạn đang thực hiện trong phương thức đó sẽ được chạy bên trong một transaction.

    Transaction sẽ thất bại khi một Exception trong một trong những truy vấn trong Transaction đó xảy ra.

    @Dao
    abstract class UserDao {
        
        @Transaction
        open fun updateData(users: List<User>) {
            deleteAllUsers()
            insertAll(users)
        }
    
        @Insert
        abstract fun insertAll(users: List<User>)
    
        @Query("DELETE FROM Users")
        abstract fun deleteAllUsers()
    }
    

    Bạn cũng có thể sử dụng @Transaction cho các phương thức @Query có câu lệnh chọn, trong các trường hợp sau:

    • Khi kết quả của truy vấn khá lớn. Bằng cách truy vấn cơ sở dữ liệu trong một giao dịch, bạn đảm bảo rằng nếu kết quả truy vấn không vừa với single cursor window, thì nó sẽ không bị hỏng do những thay đổi trong cơ sở dữ liệu giữa các lần cursor window swaps.

    • Khi kết quả của truy vấn là POJO với các trường @Relation. Các trường là các truy vấn riêng biệt nên việc chạy chúng trong một Transation sẽ đảm bảo kết quả nhất quán giữa các truy vấn.

    4. Tối ưu hoá đối tượng truy vấn

    Khi truy vấn cơ sở dữ liệu, bạn nên cân nhắc rằng có sử dụng tất cả các fields bạn trả về trong truy vấn của mình không?

    Quan tâm đến dung lượng bộ nhớ mà ứng dụng của bạn sử dụng và chỉ tải tập hợp con các trường mà bạn sẽ sử dụng. Điều này cũng sẽ cải thiện tốc độ truy vấn của bạn bằng cách giảm IO cost.

    Room sẽ thực hiện ánh xạ giữa các Columns và Objects cho bạn.

    Cùng xem một ví dụ sau:

    @Entity(tableName = "users")
    data class User(@PrimaryKey
                    val id: String,
                    val userName: String,
                    val firstName: String, 
                    val lastName: String,
                    val email: String,
                    val dateOfBirth: Date, 
                    val registrationDate: Date)
    

    Trên một số trường hợp cụ thể, các bạn không cần hiển thị tất cả thông tin này. Vì vậy, thay vào đó, chúng ta có thể tạo một đối tượng UserMinimal chỉ chứa dữ liệu cần thiết.

    data class UserMinimal(val userId: String,
                           val firstName: String, 
                           val lastName: String)
    

    Trong lớp DAO, bạn chỉ cần xác định truy vấn như sau.

    @Dao
    interface UserDao {
        @Query(“SELECT userId, firstName, lastName FROM Users)
        fun getUsersMinimal(): List<UserMinimal>
    }
    

    5. Khởi tạo trước dữ liệu cho Room Database

    Có rất nhiều trường hợp cụ thể mà bạn muốn thiết lập một bộ dữ liệu Default vào cơ sở dữ liệu trước. Khi đó bạn nên cân nhắc đến việc chèn dữ liệu ngay sau khi Room được khởi tạo.

    Cụ thể, bạn có thể cân nhắc sử dụng RoomDatabase#Callback! Gọi phương thức addCallback khi xây dựng RoomDatabase của bạn và ghi đè onCreate hoặc onOpen.

    • onCreate will be called when the database is created for the first time, after the tables have been created.
    • onOpen is called when the database was opened.
       companion object {
    
            @Volatile private var INSTANCE: DataDatabase? = null
    
            fun getInstance(context: Context): DataDatabase =
                    INSTANCE ?: synchronized(this) {
                        INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
                    }
    
            private fun buildDatabase(context: Context) =
                    Room.databaseBuilder(context.applicationContext,
                            DataDatabase::class.java, "Test.db")
                            // prepopulate the database after onCreate was called
                            .addCallback(object : Callback() {
                                override fun onCreate(db: SupportSQLiteDatabase) {
                                    super.onCreate(db)
                                    // insert the data on the IO Thread
                                    ioThread {
                                        getInstance(context).dataDao().insertData(PRE_POPULATE_DATA)
                                    }
                                }
                            })
                            .build()
    
            val PRE_POPULATE_DATA = listOf(Data("1", "val"), Data("2", "val 2"))
        }
    
    private val IO_EXECUTOR = Executors.newSingleThreadExecutor()
    
    /**
     * Utility method to run blocks on a dedicated background thread, used for io/database work.
     */
    fun ioThread(f : () -> Unit) {
        IO_EXECUTOR.execute(f)
    }
    

    Note: Các bạn chú ý thực hiện việc Insert default data trên một Worker Thread.

    Trên đây là một số Tips nhỏ trong việc sử dụng và triển khai Room Database. Mong là ít nhiều sẽ giúp ích cho mọi người. Cảm ơn mọi người đã theo dõi.

  • Code sướng tay: Bước 2

    Code sướng tay: Bước 2

    Hi, hôm nay mình lại tiếp tục xàm xí đâyyy. Hôm nay sẽ lại tiếp tục về chuyện đặt tên và thêm về cách viết hàm. Hãy bắt đầu với 1 câu nói sến súa nào :3

    Programs must be written for people to read, and only incidentally for machines to execute.

    (Đại khái là code viết cho người đọc, chỉ là vô tình máy chạy được thôi)

    Abelson and Sussman

    Và để người đọc được thì sao không viết code một cách có-thể-đọc-được nhỉ?

    // Kiểu như này
    // "If errors is empty..."
    if (errors.isEmpty) ...
    
    // "Hey, subscription, cancel!"
    subscription.cancel();
    
    // "Get the monsters where the monster has claws."
    monsters.where((monster) => monster.hasClaws);
    // Thay vì như này
    // Telling errors to empty itself, or asking if it is?
    if (errors.empty) ...
    
    // Toggle what? To what?
    subscription.toggle();
    
    // Filter the monsters with claws *out* or include *only* those?
    monsters.filter((monster) => monster.hasClaws);

    Tuy nhiên đừng cố gắng để nó quá dễ đọc đến mức như đang nói, điều này làm code rườm rà và trở nên khó đọc hơn bao giờ hết!

    // has gone too far
    if (theCollectionOfErrors.isEmpty) ...
    
    monsters.producesANewSequenceWhereEach((monster) => monster.hasClaws);

    Cách đặt tên cho câu hỏi yes/no

    Tên trả lời cho câu hỏi yes/no thường được dùng làm các điều kiện rẽ nhánh, so sánh 2 cách viết sau:

    if (window.closeable) ...  // Adjective.
    if (window.canClose) ...   // Verb.

    Chúng ta có thể đặt tên như sau:

    • Bắt đầu với “to be”isEnabledwasShownwillFire. Đây là cách phổ biến nhất (window.closeable cũng có thể được đặt là window.isCloseable)
    • Sử dụng trợ động từhasElementscanCloseshouldConsumemustSave.
    • Các động từ thông thường không hay được dùng trong TH này

    Chúng ta có thể đặt tên bắt đầu với những động từ này mà không bị nhầm lẫn với các hàm do nó không phải là các động từ mệnh lệnh.

    // Nên đặt như này
    // Tuy nhiên, trong một số trường hợp
    // bỏ qua các tiền tố yes/ no này mà nghĩa vẫn rõ ràng thì nên bỏ đi
    isEmpty
    hasElements
    canClose
    closesWindow
    canShowPopup
    hasShownPopup
    // Chứ không phải là như này
    empty         // Adjective or verb?
    withElements  // Sounds like it might hold elements.
    closeable     // Sounds like an interface.
                  // "canClose" reads better as a sentence.
    closingWindow // Returns a bool or a window?
    showPopup     // Sounds like it shows the popup.

    Tiếp theo là về việc lùi dòng và độ phức tạp của code trong phạm vi hàm. Mỗi lần bạn lùi vào 1 tab, vậy là code của bạn đã phức tạp và tốn công sức để đọc thêm 1 bậc. Mình rất thích đọc và xử lí hết lỗi trong tab Problems này cho đến khi nó hiện lên dòng chữ như vầy:

    Không còn lỗi nữa~

    Và khi mình code js, đã có rất nhiều cảnh báo bắn ra pằng pằng khi mình code của mình quá dài, nó sẽ yêu cầu mình tách hàm ra (tính ra IDE giúp được rất nhiều, nó còn có thể detect các đoạn code giống nhau mà mình copy paste sau đó bảo mình tách hàm đi bạn ơi~). IDE sẽ cảnh báo khi bạn lùi vào quá nhiều và yêu cầu bạn refactor code. IDE làm vậy không phải vì nó không đọc được code của bạn, mà vì nó sợ sau này bạn sẽ không đọc được code của bạn đấy :P. Vậy tại sao trong 1 hàm lại cần tab vào nhiều đến thế? Đó là vì hàm đó đang cố gắng làm quá nhiều việc.

    Có một nguyên lí rất nổi tiếng trong giới lập trình viên, gọi là Single Responsibility Principle (nguyên lí đơn nhiệm). Nguyên lí này khi áp dụng cho một hàm thì có nghĩa là: 1 hàm chỉ nên làm 1 việc. Đôi khi 1 việc này sẽ hơi tốn code, nhưng trong hầu hết các trường hợp (nếu không phải bạn đang thực thi một thuật toán gì đó phức tạp) thì nó sẽ có thể viết trong chiều dài màn hình của bạn, và không quá 3 lần tab vào (theo ý kiến của mình, hoặc số lần tab vào này có thể phụ thuộc vào trí nhớ của bạn nhưng với mình 4 là quá nhiều để nhớ, nếu mỗi lần tab vào đi kèm 1 điều kiện thì đã có 16 trường hợp nhỏ nhất đang chờ được mình duyệt qua T_T). Có 1 tip nhỏ để giảm bớt độ phức tạp lùi dòng, đó là sử dụng early return (1 lần duy nhất) trong hàm của bạn. Điều này sẽ giảm tab complexity xuống 1 level rồi đó :3

    Early Return (thoát sớm)

    Tuy nhiên, không phải lúc nào chúng ta cũng nên chia ra quá nhiều hàm với 1 đống param đi kèm, điều này sẽ tạo ra rất nhiều các hàm ăn bám (hàm chỉ dùng được duy nhất 1 lần cho 1 hàm khác) và thừa thãi. Hãy tách hàm sao cho hợp lí để dễ dàng đọc (và đôi khi là dùng) chúng nhé :3

    Chúc mọi người sẽ code sướng cái tay và đọc code sướng con mắt hehe. À với Tết rồi nên chúc ai đọc bài viết này sẽ đẹp trai/xinh gái, nhiều tiền, nhiều sức khoẻ, vui vẻ, hạnh phúc và không bị đồng nghiệp (hoặc sếp) chửi vì code lởm ạ.

    Tham khảo:

    https://dart.dev/guides/language/effective-dart/design

    https://stackoverflow.com/questions/475675/when-is-a-function-too-long

  • Code sướng tay: Bước 1

    Có một vấn đề mà ai code cũng cần làm, đó là đặt tên (cho lớp, hàm, biến…).

    Ai cũng có thể viết code máy hiểu được, nhưng chỉ số ít viết code mà người hiểu được.

    “I’m not a great programmer, I’m just a good programmer with great habits.”

    Martin Fowler (tác giả sách Refactoring)

    Hồi còn đi học, chúng ta hay làm toán và đặt tên các biến là x,y,z,t… các hằng là a,b,c,…m,n…. Chúng ta hay giải lí với các biến là U, I, g, … Tương tự với các môn cần đến toán như hóa, sinh… Và tin học cũng là một môn dùng đến toán như vậy. Chúng ta tiếp tục lặp lại những gì mình từng biết:

    int a, b, c;

    float x, y, z;

    double aa, bb, xx, yy;

    Đặt những cái tên mà chúng ta thấy là đúng đắn, nhưng trông thật vô nghĩa. Trước mình cũng code rất nhiều những thuật toán, mà giờ đọc lại, không còn hiểu được ngày xưa mình code gì thế này, hàm này để làm gì, giải quyết bài toàn gì? Đừng đặt cho biến của bạn những cái tên quá ngắn như thế, vì nó chẳng thể hiện được tí ý nghĩa nào của biến đó hết.

    Đố ai hiểu code đang nghĩ gì :v

    Sau một chút thì mình học được cách thêm comment vào code để dễ hiểu hơn, như thế này:

    code cùng một chút comment (l=left, r=right)

    Tuy nhiên, sau này mấy anh khóa trên đi làm xong đến lớp bảo: “Ở công ty code giờ về lớp code nhìn cứ ngứa mắt. Đi làm code là tên biến nó cứ dài dài cơ. Càng dài càng tốt ae nhề, dài nhìn mới thuận mắt (nói với mấy anh cùng đi làm).” Lúc đấy mình cũng cười trừ thôi, vì thấy code của mình đọc dễ hiểu lắm rồi, đặt tên dài làm gì đâu. Hồi đó mình còn học các cách để rút gọn/ viết tắt tên biến như là lấy chữ cái đầu, bỏ đi nguyên âm, lấy 3 kí tự đầu… Mãi sau mới thấy, IDE gợi ý đến tận răng, tiếc gì mấy phím gõ mà phải học viết tắt quá mức như vậy nhỉ (thật ra học cái này cũng hữu ích khi mà đọc code người khác họ viết lst còn biết là list chả hạn .-.) Đừng cố gắng viết tắt tên biến, trừ khi là những từ viết tắt mà ai cũng biết và hiểu rõ.

    Hóa ra là, hàng tốt thì không cần quảng cáo, code tốt thì không cần comment, tự những cái tên trong code cần phải làm việc của nó khiến đoạn code dễ đọc, dễ hiểu (trừ những logic phức tạp và khó hiểu). Khi tên một hàm thể hiện nó làm gì, người ta sẽ không cần đọc đến đoạn code bên trong nó nữa. Trong cuộc đời 1 coder, thời gian đọc code luôn nhiều hơn thời gian viết ra nó (vì code thường sẽ cần được sửa), nên nếu đặt một cái tên tốt, sau này đọc lại bạn sẽ đỡ áp lực hơn khi đọc lại code của chính mình (và đỡ muốn đấm bản thân hơn, đỡ mất thời gian đọc comment, đỡ phải vò đầu bứt tai suy nghĩ ôi cái đoạn code này dùng để làm gì thế nhỉ).

    Nếu biến của bạn có đơn vị, hãy cố gắng thêm đơn vị vào tên của nó. Ví dụ thay vì hàm startAnimation(int delayTime) hãy đặt là startAnimation(int delayTimeInSeconds) hoặc nếu tốt hơn nữa, hãy thay kiểu của nó thành một kiểu mà sẽ khó bị nhầm lẫn, ví dụ như Duration trong Dart, bạn sẽ không cần quan tâm đến đơn vị của nó nữa. Khi code, nếu bạn phát hiện ra một cái tên khó hiểu hoặc dễ hiểu lầm và bạn có thể đặt cho nó cái tên tốt hơn, hãy đổi nó ngay khi bạn có thể (miễn là nó không ảnh hưởng tới đồng nghiệp của bạn, nếu có ảnh hưởng, bạn có thể hỏi ý kiến họ trước hoặc thêm một chút comment để sau này không tốn thời gian quá nhiều để hiểu nó nữa). Nhỏ thôi nhưng sẽ khiến bạn của sau này phải cảm ơn bạn đấy :3

    Một số cách đặt tên mà bạn nên tham khảo:

    • Nên dùng tiếng anh để đặt tên vì hầu hết các kí hiệu trong code đều là tiếng anh và tiếng anh không dễ bị nhầm lẫn như tiếng việt không dấu
    • Tuân thủ nguyên tắc đặt tên của ngôn ngữ/ dự án mà bạn làm
    • Đặt tên cho hàm bằng các động từ, cho các lớp bằng danh từ
    • Chỉ sử dụng biến bắt đầu bằng is/ are/ has… để đặt cho các biến/ thuộc tính/ hàm trả lời cho câu hỏi yes/no
    • Khi bạn không nghĩ ra được một cái tên cho biến/hàm/lớp bạn cần vì nó dễ bị nhầm lẫn với những biến/hàm/lớp khác, đây là lúc nên đặt lại tên cho cả những biến/hàm/lớp khác kia (chứ đừng thay bằng một từ đồng nghĩa, điều này sẽ khiến mọi người bối rối vì không biết chúng khác nhau ở đâu .-.)

    Tóm tắt các nguyên tắc:

    1. Tuân thủ convention đặt tên của ngôn ngữ/dự án đang làm
    2. Tên biến nên thể hiện ý nghĩa của nó, đừng cố gắng viết tắt nó
    3. Tham khảo một số nguyên tắc chung

    Lần đầu viết bài, mong là sẽ có lần sau hehe~

  • Lottie Animation: Biến hình màn hình Login của bạn

    Lottie Animation: Biến hình màn hình Login của bạn

    Hi mọi người, ở bài viết trước mình đã hướng dẫn các bạn cách làm sao để thêm và ứng dụng Lottie vào ứng dụng của bạn. Có thể khi đọc bài viết trước các bạn sẽ chưa hình dung được sức mạnh của Lottie Animation. Để chứng minh sức mạnh của Lottie Animation hôm nay mình sẽ hướng dẫn các bạn ứng dụng nó vào màn hình Login và biến nó trở thành 1 màn hình thú vị hơn. Để hiểu rõ hơn về cách cài đặt Lottie animation bạn có thể xem lại bài viết Hướng dẫn cách sử dụng Lottie Animation trong Swift

    Chuẩn bị

    Để có thể bắt đầu dễ dàng và nhanh chóng hơn thì chúng ta cần chuẩn bị sẵn những thứ sau:

    1. Kiến thức về Lottie, tham khảo tại đây
    2. Project demo các bạn tải tại đây

    Bắt đầu

    Đầu tiên chúng ta sẽ kéo UI vào để thực hiện thay đổi màn hình Login như sau:

    Trong màn hình Login của chúng ta sẽ có các thành phần sau:

    1. ImageView làm background cho ứng dụng
    2. 2 Lottie Animation View, 1 cái làm thành phần chính nằm trên nửa màn hình và một cái nằm ở góc nhằm trang trí thêm cho màn hình
    3. 2 UITextField để cho người dùng nhập user name và password
    4. 1 button Login

    Sau khi sử dụng interface builder thực hiện kéo các view vào màn hình thì các bạn kéo reference cho các item vào view controller để thực hiện code.

    Mở ViewController lên và import Lottie

    import UIKit
    import Lottie

    Viết thêm một số func để cài đặt lottie và cài đặt view như sau

    extension ViewController {
        func setupUI() {
            tfAccount.layer.borderWidth = 1
            tfAccount.layer.borderColor = UIColor.orange.cgColor
            tfAccount.backgroundColor = UIColor.orange.withAlphaComponent(0.2)
            tfAccount.layer.cornerRadius = 5
            tfAccount.attributedPlaceholder = NSAttributedString(string: "  Account",
                                                                 attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
            tfAccount.placeholderRect(forBounds: CGRect(x: 5, y: 5, width: 100, height: 44))
            tfPassword.layer.borderWidth = 1
            tfPassword.layer.borderColor = UIColor.orange.cgColor
            tfPassword.backgroundColor = UIColor.orange.withAlphaComponent(0.2)
            tfPassword.layer.cornerRadius = 5
            tfPassword.attributedPlaceholder = NSAttributedString(string: "  Password",
                                                                 attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
            btnLogin.layer.cornerRadius = 5
            loadingView.backgroundColor = UIColor.black.withAlphaComponent(0.4)
        }
        
        func setupLottie() {
            // 1. Set animation content mode
            animationLottieView.contentMode = .scaleAspectFit
            animationLottieView2.contentMode = .scaleAspectFit
    
            // 2. Set animation loop mode
    
            animationLottieView.loopMode = .loop
            animationLottieView2.loopMode = .loop
    
            // 3. Adjust animation speed
    
            animationLottieView.animationSpeed = 0.5
            animationLottieView2.animationSpeed = 0.5
    
            // 4. Play animation
            animationLottieView.play()
            animationLottieView2.play()
        }
        
        private func showLoading() {
            // 1. Create lottie animation view
            let lottieView: LottieAnimationView = LottieAnimationView(name: "loading_crypto")
            lottieView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
            
            // 2. Set animation content mode
            lottieView.contentMode = .scaleAspectFit
            
            // 3. Set animation loop mode
            lottieView.loopMode = .loop
            
            // 4. Adjust animation speed
            lottieView.animationSpeed = 0.5
            
            loadingView.addSubview(lottieView)
            lottieView.center = CGPoint(x: UIScreen.main.bounds.width / 2, y:  UIScreen.main.bounds.height / 2)
            view.addSubview(loadingView)
            // 5. Play animation
            lottieView.play()
            
        }
        
        @objc private func hideLoading() {
            loadingView.subviews.forEach { $0.removeFromSuperview() }
            loadingView.removeFromSuperview()
        }
    }

    Thêm loading view để thực hiện màn hình loading như sau

    class ViewController: UIViewController {
        @IBOutlet weak var tfAccount: UITextField!
        @IBOutlet weak var tfPassword: UITextField!
        @IBOutlet weak var animationLottieView: LottieAnimationView!
        @IBOutlet weak var btnLogin: UIButton!
        @IBOutlet weak var animationLottieView2: LottieAnimationView!
        
        private var loadingView: UIView = UIView.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            setupUI()
            setupLottie()
        }
        
        @IBAction func login(_ sender: Any) {
            showLoading()
            perform(#selector(hideLoading), with: nil, afterDelay: 5)
        }
    }

    ở viewDidLoad chúng ta sẽ thực hiện setUpUI và setup lottie, tại func login chúng ta sẽ thực hiện showLoading và để nó ẩn đi sau 5 giây.

    Bây giờ chúng ta thực hiện cmd + R để chạy ứng dụng, kết quả thu được sẽ như sau:

    Kết quả thật tuyệt vời đúng không? Đối với các màn hình có animation phức tạp như vậy để code sử dụng animation gần như ta quá phức tạp và tốn quá nhiều effort để thực hiện, tuy nhiên nếu dùng lottie các bạn chỉ mất một chút thời gian để căn chỉnh layout.

    Vậy là chúng ta đã hoàn thành việc ứng dụng Lottie vào màn hình Login để tăng trải nghiệm của người dùng. Ngoài ra chúng ta còn có thể ứng dụng Lottie vào rất nhiều các tính năng khác để ứng dụng trở nên thú vị hơn. Ví dụ như: Màn hình launch screen với animation logo, animation trên notification, animation Tabbar icon, v.v.

    Cách sử dụng Lottie phụ thuộc rất lớn vào trí tưởng tượng và óc sáng tạo của người dùng, nếu bạn chỉ nắm vai trò là front end developer nhiều khi bạn sẽ không có quyền quyết định ứng dụng sẽ như nào, nhưng nếu bạn có ý tưởng hay ho bạn có thể đưa ý kiến lên Team leader, designer, BA, và PM của dự án để cải thiện trải nghiệm của người dùng.

    Chúc các bạn thành công với các dự án sắp tới.

  • Localization: Tối ưu hoá quá trình khi làm ứng hỗ trợ đa ngôn ngữ

    Localization: Tối ưu hoá quá trình khi làm ứng hỗ trợ đa ngôn ngữ

    Như mọi người đã biết, hầu hết các ứng dụng ngày nay đều hỗ trợ tính năng đa ngôn ngữ, nhằm mục đích tiếp cận được nhiều người dùng hơn, cho mọi người sử dụng ứng dụng dễ dàng hơn. Tuy nhiên hiện nay hầu hết các dự án đều làm một cách tự phát, chưa có một quy trình chuẩn. Điều này làm cho quá trình phát triển sinh ra nhiều bug UI sai Message, sai chính tả, nó làm tốn thời gian không đáng có của các bên.

    Trong quá trình làm việc ở các dự án, khi tài liệu được cập nhật đồng nghĩa với việc các Dev phải quay lại kiểm tra các thay đổi message, rồi bắt đầu loay hoay tìm kiếm trong source code để thực hiện thay đổi.

    Hay mỗi khi code một màn hình mới việc phải định nghĩa một đống key, định nghĩa enum/constant và copy/paste nó sang các file localization thật nhàm chán và tiềm tàng nhiều rủi ro về lỗi sai chính tả, copy nhầm. Trước đây, có một member trong dự án của mình chỉ vì copy sai text làm thừa một dấu “;” làm lỗi cả file đa ngôn ngữ, việc này làm cho toàn bộ text trên ứng dụng khi release cho bị sai. Do file đa ngôn ngữ là string nên những lỗi sai chính tả như này mất rất nhiều thời gian để điều tra lỗi.

    Sau nhiều năm làm việc với các dự án khác nhau, cùng các anh em, cộng sự hoàn thành các ứng dụng to nhỏ khác nhau. Mình đã đúc kết được một quy trình giúp cải thiện năng suất làm việc của mọi người, giúp giảm các bug UI về sai message, sai chính tả, giảm thời gian thực hiện tính năng đa ngôn ngữ, giúp quá trình bảo trì, thay đổi message trở nên dễ dàng hơn.

    Quy trình

    Brainstoming

    Trước tiên, để đạt được hiệu quả Leader của các bên gồm BA, Mobile, Web … ngồi lại với nhau để thống nhất về cách làm việc theo quy trình trên. Sau đó phổ biến lại cho member và đảm bảo việc member thực hiện đúng quy trình làm việc.

    Tạo file excel chung lưu các message cần làm đa ngôn ngữ

    File message template các bạn tải ở đây nhé:

    Trong file này mình có định nghĩa sẵn các trường cần nhập và có sẵn công thức để gen ra code của swift và android. Nếu dự án của các bạn sử dụng ngôn ngữ khác thì có thể chỉnh lại công thức cho phù hợp với dự án của mình.

    Nhập nội dung

    Thêm/sửa message

    Screen/Feature: Điền vào tiên màn hình hoặc tính năng.

    MessageKey: Điền vào tên item trên màn hình, nếu SRS có mô tả thì ưu tiên sử dụng key trong SRS, nếu không hãy đặt tên sao cho đúng ý nghĩa của item.

    Tiếng việt: Text được hiển thị khi ngôn ngữ là Tiếng Việt

    Tiếng anh: Text được hiển thị khi ngôn ngữ là Tiếng Anh

    Note:
    TH1. Tài liệu đã định nghĩa sẵn các message cho các ngôn ngữ: thì BA có thể cử 1 bạn ra điền hết vào file này trước khi bên dev thực hiện code. Trường hợp này thì việc làm đa ngôn ngữ khá nhàn, mình sẽ giải thích chi tiết ở các phần phía dưới.
    TH2: Tài liệu chưa định nghĩa message cho các ngôn ngữ(các dự án thường rơi vào trường hợp này): Khi này khi các Dev thực hiện code sẽ tự insert thêm message vào file trên teams, để file tự tạo ra code tương ứng với các ngôn ngữ.

    Công thức tạo code tự động

    Công thức tạo code từ excel

    Các Developers sẽ không phải làm các thao tác lặp đi lặp lại nhàm chán. Ngoài ra nó giúp giảm lỗi đánh máy, lỗi copy paste, lỗi sai chính tả và giúp việc làm đa ngôn ngữ nhanh hơn. Trên file excel mình có tạo ra công thức để tự tạo ra code của swift/android. Khi bạn điền thông tin vào sheet MessageList thì code sẽ được gen tự động, việc của mọi người là chỉ cần copy code vào project và sử dụng.

    Đưa ra các luật lệ

    Tạo ra các luật yêu cầu member phải tuân thủ như sau:

    1. Đặt tên màn hình theo quy định của dự án, cách đặt tên cần thống nhất giữa tài liệu SRS và các class trong code của Dev.
    2. Dev của các bên(mobile, web) và BA khi thực hiện thêm mới, hay chỉnh sửa sẽ thực hiện trực tiếp trên files Localize chung của dự án, khi chỉnh sửa cần tìm đúng tên màn hình, message để sửa, nếu thêm mới thì thêm xuống cuối cùng của của danh sách list message của màn hình đó.
    3. Để tránh việc conflict hoặc giẫm chân nhau khi các bên chạy song song, thì mỗi màn hình những member liên quan tới màn hình đó sẽ cử ra một bạn điển vào file message trước.

    Cách thực hiện code

    TH1: đối với dự án không cho sử dụng thư viện mã nguồn mở

    Khi thực hiện chúng ta chỉ cần vào file excel và copy code vào project của mình rồi sử dụng bình thường.

    TH2: Dự án cho phép sử dụng thư viện mã nguồn mở

    Chúng ta sẽ sử dụng thư viện để tự tạo ra enum nhanh gọn lẹ hơn, ví dụ bên iOS Swift thì chúng ta có thể sử dụng Swiftgen để thực hiện. Link hướng dẫn sử dụng Swiftgen mình cũng có viết môt bài rồi các bạn có thể tham khảo link dưới đây:

    Ưu điểm

    • Giảm thời gian làm việc, tăng năng suất làm việc của team
    • Giảm khả năng bị lỗi UI/Message hay các lỗi về copy/paste khi thực hiện
    • Giảm thời gian bảo trì ứng dụng khi thay đổi message, khi có thay đổi BA sẽ update file message list và báo lại cho Devs, lúc này Dev không cần phải tìm xem thay đổi ở đâu, mà chỉ cần copy code từ file message list là xong.
    • Giảm thời gian implement khi có yêu cầu hỗ trợ thêm ngôn ngữ khác. Vì khi này chúng ta chỉ cần thêm 1 cột nữa ở file excel rồi copy code sang project là xong.
    • Giúp tối ưu được effort khi các team không start cùng một thời điểm. Vì team start sau sẽ sử dụng lại được phần đã làm của team start trước.

    Nhược điểm

    • Cần quản lí, phân chia công việc tốt, member có khả năng làm việc nhóm.
    • Thường chỉ hiệu quả cao đối với các dự án mới

    Tổng kết

    Phía trên là nội dung chia sẻ về cách tối ưu khi làm ứng dụng hỗ trợ đa ngôn ngữ, mình hi vọng bài viết có thể giúp mọi người phần nào trong quá trình thực hiện các ứng dụng có hỗ trợ đa ngôn ngữ. Cảm ơn mọi người đã dành thời gian đọc bài viết này.

  • Swift: weak and unowned

    Swift: weak and unowned

       

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

       

    1-ARC

    Automatic Reference Counting aka ARC, là 1 tính năng của Swift dùng để đếm số lượng strong reference, và cho phép quản lý việc giải phóng memory khi 1 instance không còn được reference(tham chiếu) đến.

    Theo như doc của Apple:

    Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you don’t need to think about memory management yourself. ARC automatically frees up the memory used by class instances when those instances are no longer needed.

    Chúng ta cũng cần nhớ rằng trong swift một reference của được định nghĩa mặc định là kiểu strong

    Ví dụ : 

    class FirstClass: UIViewController {
    
        let secondClass = SecondClass()
    
    }
    
    class SecondClass {
    
    }
    

    Trong ví dụ này, class FirstViewController của mình đang strong reference đến instance secondClass

       

    2-Vấn đề của strong reference cycle

    Như mình đã đề cập bên trên, ARC sẽ giúp chúng ta quản lý, phân bổ memory từ những instance không còn được tham chiếu đến.

    Tuy nhiên, câu hỏi đặt ra là điều gì sẽ xảy ra khi có 2 object/instance strong reference đến nhau?

    Từ strong việt hóa ra là mạnh, bền, … nghe thôi cũng cảm giác khó phá hỏng, phá hủy nó rồi đúng không? :)))

    Thì strong reference cũng thế, strong reference trỏ đến 1 instance/object và sở hữu nó, quyết định đến sự tồn tại của instance/object đó. 

    ARC không thể có cách nào giải phóng được memory từ những kiểu instace/object này, và điều này sẽ dẫn đến memory leak.

    Trong thực tế những project đã làm, thì mình rất hay thường gặp những case strong reference, và mình cũng rất hay vô tình tạo ra strong reference :v 

    Case mình hay gặp nhất là case khi khởi tạo delegate:

    protocol DemoDelegate {
        func demoFunc()
    }
    
    class FirstClass {
        var delegate: DemoDelegate?
    }
    

    Trông ví dụ này có vẻ quen đúng không? :))) ở đây mình có 1 protocol DemoDelegate, và khởi tạo delegate này trong FirstClass. Và FirstClass và delegate DemoDelegate đang strong reference đến nhau.

    Để giải quyết vấn đề này, chúng ta có weak và unowned.

     

    Weak và Unowned

     

    1. Weak

    Trái ngược với strong pointer, weak pointer trỏ đến một instance/object và không quyết định đến sự tồn tại của instance/object đó.

    Vì thế nên ARC có thể dễ dàng giải phóng bộ nhớ từ instance/object này kể cả chúng ta vẫn còn đang tham chiếu đến nó 

    => Weak reference rất hữu ich, giúp chúng ra tránh được việc vô tình hay cố ý tạo ra 1 strong reference cycle.

    Note: Weak reference không thể sử dụng với việc khởi tạo instance/object là let bởi vì instance/object ở một vài case vẫn phải truyền nil.

    Việc sử dụng weak reference thực sự quan trọng, ví dụ nếu chúng ta để ý hơn và xem source code bên trong definition của UITableView, ta có thể nhận thấy rằng tất cả delegate(bao gồm 2 var quen thuộc là dataSource và delegate) đều được sử dụng với weak khi khởi tạo.

    Và mình nghĩ là đến apple còn để ý và chú trọng đến việc sử dụng weak, tại sao chúng ta lại không làm như thế ? :v

    protocol DemoDelegate {
        func demoFunc()
    }
    
    class FirstClass {
        weak var delegate: DemoDelegate?
    }
    

    Ok, thêm weak là xong, đơn giản phải không nào.

    Tuy nhiên, khi thêm weak, lại nảy sinh vấn đề không thể compile đoạn code:

    Để giải quyết, theo doc Protocols của apple:

    Use a class-only protocol when the behavior defined by that protocol’s requirements assumes or requires that a conforming type has reference semantics rather than value semantics.

    Để fix, chỉ cần thêm keyword class là được

    protocol DemoDelegate: class {
        func demoFunc()
    }
    
    class FirstClass {
        weak var delegate: DemoDelegate?
    }
    

      2. unowned

    Một variable kiểu unownevề cơ bản giống với variable kiểu weak, nhưng khác nhau ở chỗ: compiler sẽ make sure rằng variable này khi được gọi đến sẽ không có giá trị nil.

    Vậy thực sự trong trường hợp nào nên sử dụng unowned thay vì sử dụng weak ? Vẫn theo doc của Apple: 

    Like a weak reference, an unowned reference doesn’t keep a strong hold on the instance it refers to. Unlike a weak reference, however, an unowned reference is used when the other instance has the same lifetime or a longer lifetime.

    Để hiểu rõ hơn, mình có 2 ví dụ như này

    Ví dụ của weak reference: mình sở hữu 1 chiếc xe đạp, nhưng 1 hôm đẹp trời nào đó, mình đổi ý không muốn đạp xe nữa, thì ở đây chiếc xe đạp vẫn còn đó, có chăng chỉ là đổi chủ, trong khi mình đang ngồi 1 chiếc 4 bánh nào đó :v 

    Ví dụ của unowned reference: mình chơi 1 yasuo chẳng hạn :))), thằng nhân vật của mình tăng skill theo cấp độ. Tuy nhiên, khi xám màn hình, những skill đó cũng sẽ xám theo. Có nghĩa rằng là những skill này có tuổi thọ = tuổi thọ của nhân vật mình đang chơi.

    Triển khai trong code:

    class Yasuo {
    
        let name: String
        let skill: Skill?
    
        init(name: String) {
            self.name = name
        }
    
    }
    
    class Skill {
    
        let damage: Int
        unowned let champ: Yasuo
    
        init(damage: Int, champ: Yasuo) {
            self.damage= power
            self.champ = champ
        }
    
    }
    

       

    3-Debugging

    Vậy là chúng ta đã hiểu phần nào về 2 keyword weak và unowned. Chúng dùng để tránh tình trạng vô tình tạo ra memory leak. 

    Nhưng sao để biết được có thực sự tránh được hay không?

    Cách đơn giản và xưa như quả đất, là sử dụng print trong 2 method init và deinit

    class FirstClass {
    
        init() {
            print("FirstClass is being initialized")
        }
    
        deinit { 
            print("FirstClass is being deinitialized") 
        }
    
    }
    

    Và để ý memory trong XCode nữa :v

       

    4-Tổng kết

    Strong, weak, unowned là những thuật ngữ cơ bản trong swift, tuy nhiên chúng ta thường – do vô ý hoặc chủ quan – bỏ qua chúng.

    Điều này không thực sự ảnh hưởng quá lớn với những project nhỏ. 

    Tuy nhiên với những app lớn, việc quản lý bộ nhớ ra sao cho hiệu quả là 1 việc quan trọng và đáng lưu ý, bởi vì khi memory leak trở thành 1 vấn đề nghiêm trọng, thì rất khó và tốn effort để fix. 😀

    HAPPY CODING!

    REFERENCE

    https://docs.swift.org/swift-book/LanguageGuide/Protocols.html

    https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

  • Tìm hiểu về Regular Expression

    Tìm hiểu về Regular Expression

    Tìm hiểu về Regular Expression

    Mở đầu

    RegEx là một khái niệm chúng ta thường xuyên gặp qua. Các master thì sẽ chẳng lạ lùng gì, còn với newbie thì cực kỳ khó hiểu và tiếp cận.

    Bài viết này mình sẽ giới thiệu qua về RegEx. Khi thành thạo được công cụ này, chắc chắn cuộc sống của lập trình viên sẽ "tươi đẹp" hơn rất nhiều.

    RegEx là gì ?

    RegEx (Regular Expression) trong tiếng Việt được gọi là Biểu thức chính quy. Regex không phải là một ngôn ngữ lập trình, nó chỉ là một bộ cú pháp dùng để bắt chuỗi. Regex là các ký tự được kết hợp với nhau theo quy tắc để tạo nên một trình tự giúp chúng ta tìm kiếm và thay thế văn bản một cách thông minh, nhanh chóng, đơn giản và thuận tiện. Nó có thể dùng được trong hầu hết các ngôn ngữ lập trình bậc cao như Java, C#, Python, JS, PHP,…

    Ứng dụng của Regular Expression

    • Ứng dụng trong kiểm tra tính hợp lệ
      • Kiểm tra email có hợp lệ hay không
      • Kiểm tra số điện thoại ở Việt Nam
      • Kiểm tra URL hợp lệ
      • Kiểm tra độ dài của câu có nằm trong khoảng (a,b) hay không
      • Bất cứ cái gì có quy tắc bạn đều có thể kiểm tra với Regex
    • Ứng dụng trong tìm kiếm và thay thế

    Cú pháp của biểu thức chính quy

    Chuỗi so khớp cơ bản

    Chuỗi Ý nghĩa
    . Khớp bất kỳ ký tự nào
    \d Khớp với số bất kỳ từ 0 – 9
    \D Phủ định của \d
    \w Khớp với các chữ cái tiếng anh, chữ số và dấu _
    \W Phủ định của \w
    <dấu cách> Khớp với dấu cách(SPACE trên bàn phím)
    \t Khớp với dấu tab
    \n Khớp với new line(xuống hàng)
    \s Khớp với dấu trắng bất kỳ(dấu cách, \t,\n)
    \S Phủ định của \s

    Kết hợp các chuỗi so khớp

    Bạn có thể kết hợp các chuỗi so khớp lại với nhau bằng cách đưa chúng vào trong cặp ngoặc vuông. Ví dụ :

    • [abc] -> Khớp các ký tự hoặc là a, hoặc là b, hoặc là c
    • [a-fA-Z] -> a-f là tất cả các ký tự từ a đến f trong bảng chữ cái tiếng anh đó, A-Z tương tự là từ A hoa đến Z hoa. Vậy là regex này khớp với mọi ký tự trong bảng chữ cái tiếng anh bất kể hoa thường.
    • [\d,] -> Khớp với ký tự số hoặc dấu ,

    Các ký tự ranh giới

    Ký tự Ý nghĩa Demo
    \b Xác định ranh giới của từ demo
    \B Phủ định của \b demo
    ^ Xác định vị trí bắt đầu dòng demo
    $ Xác định vị trí kết thúc dòng demo

    Sử|+ dụng hoặc trong regex

    (0|84|+84) : Hoặc 0, hoặc 84, hoặc +84 – demo

    Các ký tự định lượng

    Loại định lượng Ý nghĩa Demo
    X* So khớp lặp lại regex X 0 hoặc vô số lần demo
    X+ So khớp lặp lại regex X 1 hoặc vô số lần demo
    X? So khớp lặp lại regex X 0 hoặc 1 lần demo
    X{m} So khớp lặp lại regex X chính xác m lần demo
    X{m,} So khớp lặp lại regex X m hoặc nhiều hơn m lần demo
    X{m,n} So khớp lặp lại regex X với số lần từ m tới n(bao gồm cả m và n) demo

    Và một số ví dụ sau:

    • ab+ : khớp các chuỗi ab, abb, abbaabab,…
    • (ab)+ : khớp các chuỗi ab, abab, ababab,…

    Các ký tự đặc biệt

    Các ký tự {}[]()^$.|*+?\ và dấu - ở trong cặp ngoặc vuông là các ký tự đặc biệt. Nếu bạn muốn dùng nó để so khớp thì cần thêm dấu \ vào đằng trước:

    Ví dụ: \. sẽ khớp với dấu chấm .\\ sẽ khớp với ký tự \.

    Một số RegEx được dùng phổ biến

    So khớp email

    • ^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$
    • ^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$
    • ^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|((a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$
    • ^\w+[\w-\.]*\@\w+((-\w+)|(\w*))\.[a-z]{2,3}$
    • ^.+@.+$

    So khớp URL

    • ^((https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$
    • ((mailto\:|(news|(ht|f)tp(s?))\://){1}\S+)
    • (http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:/~\+#]*[\w\-\@?^=%&amp;/~\+#])?
    • ^(http|https|ftp)\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(:[a-zA-Z0-9]*)?/?([a-zA-Z0-9\-\._\?\,\’/\\\+&amp;%\$#\=~])*$

    So khớp SĐT ở Việt Nam

    • \+?(0|84)\d{9}

    So khớp số nguyên

    • \b\d+\b
    • \b(?:\d{3}\.)+\d{3}\bdemo

    So khớp mã HTML

    So khớp tên riêng

    • (?:[A-Z]\p{L}+ ){1,3}[A-Z]\p{L}+demo

    Một số liên kết hữu ích

    1. Test RegEx online: https://regex101.com/
    2. RegEx cheetsheet: http://web.mit.edu/hackl/www/lab/turkshop/slides/regex-cheatsheet.pdf
    3. Thư viện regex: http://regexlib.com/

    Kết bài

    Trên đây là một số kiến thức "nhập môn" về RegEx. Hi vọng bài viết nhỏ này đã giúp bạn học được cú pháp viết RegEx và áp dụng vào công việc thường ngày.

  • Upload file dung lượng lớn tới S3 với Multipart và Presign-url

    Upload file dung lượng lớn tới S3 với Multipart và Presign-url

    Nếu như bình thường ta thực hiện single upload tới s3 thì sẽ có 2 cách sau:

    • Upload file lên s3 qua server của chúng ta: Cái này thì reject vì chúng ta phải tốn thời gian upload lên server của mình rồi từ đó mới upload lên S3. Với large file thì gần như là không nên.

    • Upload từ client bằng cách sử dụng presign-url. Thay vì phải qua server trung gian thì chúng ta upload thẳng lên S3. Tốc độ cải thiện rất nhiều vì cách 1 đa phần thời gian tốn ở khâu upload lên server của chúng ta.

    Nhưng nói gì thì nói single upload to S3 thì sẽ có những hạn chế sau dưới Maximum size chỉ là 5GB . Điều này thực sự hạn chế. Hơn nữa AWS cũng suggest chúng ta là file >100MB thì nên sử dụng Multipart Upload . Vậy ưu điểm chính của nó là:

    • Maximum size là 5TB
    • Tốc độ upload tốt hơn

    Điều đó mình lựa chọn multipart và presign-url cho bài toán upload file dung lượng lớn. Server ở đây mình sử dụng là python. Client thì dùng angular.

    part1

    Server

    Phần server này mình sử dụng kiến trúc serverless để triển khai với ngôn ngữ Python Flask trên môi trường AWS Cloud

    Các bạn tham khảo project trên Github của mình để hiểu cách setup nhé.

    Phần Server chủ yếu sẽ có 3 api chính:

    • Đầu tiên là api start-upload, với logic là request tới s3 để lấy uploadId

      @app.route("/start-upload", methods=["GET"])
      def start_upload():
          file_name = request.args.get('file_name')
          response = s3.create_multipart_upload(
              Bucket=BUCKET_NAME, 
              Key=file_name
          )
      
          return jsonify({
              'upload_id': response['UploadId']
          })
      
    • Tiếp theo là api get-upload-url để lấy presignurl cho từng part của file khi upload

      @app.route("/get-upload-url", methods=["GET"])
      def get_upload_url():
           file_name = request.args.get('file_name')
           upload_id = request.args.get('upload_id')
           part_no = request.args.get('part_no')
           signed_url = s3.generate_presigned_url(
               ClientMethod ='upload_part',
               Params = {
                   'Bucket': BUCKET_NAME,
                   'Key': file_name, 
                   'UploadId': upload_id, 
                   'PartNumber': int(part_no)
               }
           )
      
           return jsonify({
               'upload_signed_url': signed_url
           })
      
    • Cuối cùng đây là api để kiểm tra xem việc upload đã hoàn thành chưa.

       @app.route("/complete-upload", methods=["POST"])
       def complete_upload():
           file_name = request.json.get('file_name')
           upload_id = request.json.get('upload_id')
           print(request.json)
           parts = request.json.get('parts')
           response = s3.complete_multipart_upload(
               Bucket = BUCKET_NAME,
               Key = file_name,
               MultipartUpload = {'Parts': parts},
               UploadId= upload_id
           )
           
           return jsonify({
               'data': response
           })
      

    Client

    Phần client này mình dùng Angular để triển khai 1 trang web upload file đơn giản.

    Các bạn tham khảo project trên Github của mình để hiểu cách setup nhé.

    Khi có action upload đầu tiên ta sẽ gọi tới function uploadMultipartFile. Function uploadMultipartFile có chức năng với các step sau:

    • Call api start-upload để lấy được uploadId.

      const uploadStartResponse = await this.startUpload({
          fileName: file.name,
          fileType: file.type
      });
      
    • Split file upload thành các chunks, ở đây chia thành 10MB/chunk nhé. Ở đây mình chia là 10MB vì minimum size của Multipart Upload là 10MB.

    • Thực hiện call api get-upload-url để lấy preSignurl cho từng chunk ở trên.

    • Upload các chunks bằng signurl.

    • Khi tất cả các chunks upload xong sẽ thực hiện call api complete-upload để xác nhận với S3 mình đã upload đầy đủ các part. Done.

      try {
          const FILE_CHUNK_SIZE = 10000000; // 10MB
          const fileSize = file.size;
          const NUM_CHUNKS = Math.floor(fileSize / FILE_CHUNK_SIZE) + 1;
          let start, end, blob;
      
          let uploadPartsArray = [];
          let countParts = 0;
      
          let orderData = [];
      
          for (let index = 1; index < NUM_CHUNKS + 1; index++) {
            start = (index - 1) * FILE_CHUNK_SIZE;
            end = (index) * FILE_CHUNK_SIZE;
            blob = (index < NUM_CHUNKS) ? file.slice(start, end) : file.slice(start);
      
            // (1) Generate presigned URL for each part
            const uploadUrlPresigned = await this.getPresignUrl({
              fileName: file.name,
              fileType: file.type,
              partNo: index.toString(),
              uploadId: uploadStartResponse.upload_id
            });
      
            // (2) Puts each file part into the storage server
      
            orderData.push({
              presignedUrl: uploadUrlPresigned.upload_signed_url,
              index: index
            });
      
            const req = new HttpRequest('PUT', uploadUrlPresigned.upload_signed_url, blob, {
              reportProgress: true
            });
      
            this.httpClient
              .request(req)
              .subscribe((event: HttpEvent<any>) => {
                switch (event.type) {
                  case HttpEventType.UploadProgress:
                    const percentDone = Math.round(100 * event.loaded / FILE_CHUNK_SIZE);
                    this.uploadProgress$.emit({
                      progress: file.size < FILE_CHUNK_SIZE ? 100 : percentDone,
                      token: tokenEmit
                    });
                    break;
                  case HttpEventType.Response:
                    console.log('Done!');
                }
      
                // (3) Calls the CompleteMultipartUpload endpoint in the backend server
      
                if (event instanceof HttpResponse) {
                  const currentPresigned = orderData.find(item => item.presignedUrl === event.url);
      
                  countParts++;
                  uploadPartsArray.push({
                    ETag: event.headers.get('ETag').replace(/[|&;$%@"<>()+,]/g, ''),
                    PartNumber: currentPresigned.index
                  });
                  if (uploadPartsArray.length === NUM_CHUNKS) {
                    console.log(file.name)
                    console.log(uploadPartsArray)
                    console.log(uploadStartResponse.upload_id)
                    this.httpClient.post(`${this.url}/complete-upload`, {
                      file_name: encodeURIComponent(file.name),
                      parts: uploadPartsArray.sort((a, b) => {
                        return a.PartNumber - b.PartNumber;
                      }),
                      upload_id: uploadStartResponse.upload_id
                    }).toPromise()
                      .then(res => {
                        this.finishedProgress$.emit({
                          data: res
                        });
                      });
                  }
                }
              });
          }
        } catch (e) {
          console.log('error: ', e);
        }
      

    Có những chú ý sau.

    • Minimum size của Multipart Upload là 10MB. Maximum là 5TB
    • Được upload tối đã 10000 chunks cho 1 file thôi. Ví dụ ở đây mình chia 1 chunk là 10MB thì mình chỉ upload tối đa 200GB. Tương tự 1 chunk là 5GB => upload tối đa là 5TB.

    Demo

    • Cài đặt server với serverless. Vào project serverless và gõ lệnh

      sls deploy
      

      Có những chú ý sau:

      • Sau khi cài đặt NodeJS thì cài Serverless framework

        npm i -g serverless
        
      • Cần có account AWS

      • Config ở file serverless đang sử dụng aws profile mà mình đã setup ở local(khanhcd92). Nếu bạn dùng default hoặc cấu hình profile riêng thì hãy thay đổi nó nhé. Cách cấu hình aws profile.

        provider:
          name: aws
          runtime: python3.6
          stage: dev
          region: ap-southeast-1
          profile: khanhcd92
        
    • Thay đường dẫn API endpoint từ output của cài đặt server cho url trong class UploadService của project web

    • Cài đặt thư viện trong project web

      npm i
      
    • Chạy web angular ở local với lệnh

      npm start
      

      part2

    • Kéo file cần upload vào trình duyệt. Chỗ này mình sẽ dùng 1 file có dụng lượng khoảng 75M để upload.

      part3

    • Kiểm tra kết quả trên S3 part4

    Chi tiết source code các bạn xem ở đây nhé.

  • Cách cấu hình Gitlab CI chỉ kiểm tra các file thay đổi với merge request

    Cách cấu hình Gitlab CI chỉ kiểm tra các file thay đổi với merge request

    Chúng ta sẽ thường hay cấu hình chạy kiểm tra code quality cho tất các các nhánh và merge request trên Gitlab CI. Nhưng Gitlab runner job sẽ chạy scan toàn bộ source code nên sẽ mất thời gian đối với merge request. Ngoài ra đôi khi do source code cũ của khách hàng lỏm sẽ dẫn tới lúc nào merge request đó cũng sẽ bị failed.

    Dưới đây là sample về Gitlab CI chạy python lint với cấu hình chạy cho nhánh develop và merge request.

    pylint:
      stage: lint
      before_script:
        - mkdir -p public/badges public/lint
        - echo undefined > public/badges/$CI_JOB_NAME.score
        - pip install pylint-gitlab
      script:
        - pip install -r requirements.txt
        - pylint --rcfile=.pylintrc --exit-zero --output-format=text $(find -type f -name "*.py" ! -path "**/.venv/**") | tee /tmp/pylint.txt
        - sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' /tmp/pylint.txt > public/badges/$CI_JOB_NAME.score
        - pylint --exit-zero --output-format=pylint_gitlab.GitlabCodeClimateReporter $(find -type f -name "*.py" ! -path "**/.venv/**") > codeclimate.json
        - pylint --exit-zero --output-format=pylint_gitlab.GitlabPagesHtmlReporter $(find -type f -name "*.py" ! -path "**/.venv/**") > public/lint/index.html
      after_script:
        - anybadge --overwrite --label $CI_JOB_NAME --value=$(cat public/badges/$CI_JOB_NAME.score) --file=public/badges/$CI_JOB_NAME.svg 4=red 6=orange 8=yellow 10=green
        - |
          echo "Your score is: $(cat public/badges/$CI_JOB_NAME.score)"
      artifacts:
        paths:
          - public
        reports:
          codequality: codeclimate.json
        when: always
      only:
        - develop
        - merge_requests
      tags:
        - docker
    

    Để giải quyết các vấn đề trên và có thể tập trung vào chất lượng code của các bạn member tốt hơn, chúng ta có thể tạo thêm 1 config mới cho merge request event. Với config mới chỉ cần kiểm tra code quality trên các file thay đổi là được.

    pylint_merge_request:
      stage: lint
      before_script:
        - pip install pylint-gitlab
      script:
        - echo CI_COMMIT_SHA=${CI_COMMIT_SHA}
        - echo CI_MERGE_REQUEST_TARGET_BRANCH_NAME=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}
        - git fetch origin ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}
        - FILES=$(git diff --name-only ${CI_COMMIT_SHA} origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME} | grep '\.py'$)
        - echo "Changed files are $FILES"
        - pip install -r requirements.txt
        - pylint --rcfile=.pylintrc --output-format=text $FILES | tee /tmp/pylint.txt
      only:
        - merge_requests
      tags:
        - docker
    

    Hi vọng nó hữu ích với mọi người và có thể apply nó vào dự án của mình.