Month: May 2023

  • Context trong Android và những lưu ý

    Context trong Android và những lưu ý

    Chào mọi người, hôm nay mình xin chia sẻ về một chủ đề khá thông dụng trong Android, đó là Context. Mục đích của bài này đâu đấy các bạn mới tiếp cận với Android có thể hiểu đúng bản chất của Context, các loại Context và quan trọng hơn là cách cân nhắc sử dụng chúng trong từng trường hợp một cách hợp lý và chính xác nhất, tránh gây ra những lỗi không mong muốn.

    1. Khái niệm

    Mình xin trích dẫn một đoạn trong Official Document Android về Context

    Mình xin trích dẫn một đoạn trong Official Document Android về Context

    Interface to global information about an application environment. This is an abstract class whose implementation is provided by the Android system. It allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc.

    Các bạn cũng có thể hiểu cơ bản thế này

    Context là trạng thái của ứng dụng tại một thời điểm nhất định.

    Context là 1 lớp cơ bản chứa hầu hết các thông tin về môi trường ứng dụng của android, tức là mọi thao tác, tương tác với hệ điều hành đều phải thông qua lớp này.

    Context là 1 abstract class, nó cung cấp cho các lớp triển khai các phương thức truy cập vào tài nguyên của ứng dụng và hệ thống. Ví dụ như nó có thể khởi tạo và chạy các activities, broadcast, các intents….

    Nào giờ chúng ta xem xét 3 functions hay được sử dụng nhiều nhất để truy xuất Context:

    • getContext() — trả về Context được liên kết với Activity được gọi.
    • getApplicationContext() — trả về Context được liên kết với Application chứa tất cả các Activity đang chạy bên trong nó.
    • getBaseContext() — có liên quan đến ContextWrapper, được tạo xung quanh Context hiện có và cho phép thay đổi hành vi của nó. Với getBaseContext() chúng ta có thể lấy Context hiện có bên trong lớp ContextWrapper.

    Trong số này nổi bật hơn cả là getContext()getApplicationContext(). Liên quan đến getBaseContext(), có một lưu ý nhỏ là tránh sử dụng loại Context này – lớp này được triển khai khi một class extends từ ContextWrapper. Mà lớp này lại có khoảng 40 lớp con trực tiếp và không trực tiếp. Vì vậy, nên gọi trực tiếp đến getContext, Activity, Fragment… để tránh gây ra memory leak.

    2. Các loại truy cập Context

    2.1. getContext()

    Trong getContext(), Context được gắn với một Activity và vòng đời của nó. Chúng ta có thể hình dung Context là layer đứng sau Activity và nó sẽ tồn tại chừng nào Activity còn tồn tại. Thời điểm Activity chết, Context cũng vậy.

    Dưới đây là danh sách các chức năng mà Activity’s Context cung cấp cho chúng ta:

    Load Resource Values,
    Layout Inflation,
    Start an Activity,
    Show a Dialog,
    Start a Service,
    Bind to a Service,
    Send a Broadcast,
    Register BroadcastReceiver.
    

    2.2. getApplicationContext()

    Trong getApplicationContext(), Context của chúng ta được gắn với ứng dụng và vòng đời của nó. Chúng ta có thể coi nó như một lớp đằng sau toàn bộ ứng dụng. Miễn là người dùng không tắt ứng dụng, thì nó vẫn tồn tại.

    Bây giờ bạn có thể tự hỏi, đâu là sự khác biệt giữa getContext() và getApplicationContext(). Sự khác biệt là Context của ứng dụng không liên quan đến giao diện người dùng. Điều đó có nghĩa là, chúng ta không nên sử dụng nó để Inflate một Layout, start một Activity cũng như Dialog. Về phần còn lại của các chức năng từ Context của Activity, chúng cũng có sẵn trong Application Context. Vì vậy, danh sách các chức năng cho Application Context bao gồm như sau:

    Load Resource Values,
    Start a Service,
    Bind to a Service,
    Send a Broadcast,
    Register BroadcastReceiver.
    

    3. Cách sử dụng hợp lý Context trong Android

    Việc sử dụng Context phù hợp trong Android là rất quan trọng để đảm bảo ứng dụng của bạn hoạt động bình thường và tránh các sự cố như memoryleak hoặc sự cố tài nguyên. Dưới đây là một số nguyên tắc giúp bạn sử dụng ngữ cảnh phù hợp:

    • Chọn Context cụ thể theo ngữ cảnh: Chọn Context cung cấp phạm vi cần thiết cho thao tác bạn đang thực hiện. Ví dụ: nếu bạn đang tạo thành phần giao diện người dùng trong một Activity, hãy sử dụng Activity Context thay vì Application Context.
    • Lưu ý đến Lifespan của Context: Xem xét vòng đời của thành phần yêu cầu Context. Đối với các hoạt động tồn tại trong thời gian ngắn, hãy sử dụng Context phù hợp với Lifespan của Component. Ví dụ: nếu bạn cần Context trong phương thức onReceive() của BroadcastReceiver, hãy sử dụng tham số Context được cung cấp thay vì Context tồn tại lâu dài như Application Context.
    • Tránh sử dụng Context hoạt động ngoài vòng đời của nó: Hãy thận trọng khi sử dụng Context hoạt động bên ngoài vòng đời của Activity, vì nó có thể dẫn đến memoryleak. Chẳng hạn, nếu bạn lưu trữ tham chiếu đến ngữ Activity Context và sử dụng nó sau khi Activity đã bị hủy, thì điều đó có thể gây ra sự cố. Trong những trường hợp như vậy, hãy chuyển sang sử dụng Application Context để thay thế.
    • Lưu ý về bộ nhớ: Tránh lưu trữ các tham chiếu lâu dài đến các Context một cách không cần thiết, vì chúng có thể giữ một tham chiếu đến toàn bộ Activity và Application và ngăn các tài nguyên bị thu gom rác. Nếu bạn cần một Context trong một Component tồn tại lâu dài, chẳng hạn như một singleton, hãy ưu tiên sử dụng Application Context.
    • Cân nhắc sử dụng Dependency Injection: Việc sử dụng các Dependency Injection như Dagger, Hilt hoặc Koin có thể đơn giản hóa việc quản lý Context bằng cách cung cấp ngữ cảnh phù hợp khi cần. Nó giúp tách rời các thành phần và đảm bảo Context chính xác được đưa vào dựa trên phạm vi và yêu cầu của hoạt động.

    4. Kết luận

    Việc lựa chọn Context phụ thuộc vào các yêu cầu cụ thể của mã của bạn và thành phần bạn đang xử lý. Điều quan trọng là chọn Context phù hợp để tránh memoryleak, sự cố tài nguyên hoặc sự không nhất quán trong ứng dụng của bạn. Theo nguyên tắc chung, hãy cố gắng sử dụng bối cảnh hạn chế nhất có thể để hoàn thành nhiệm vụ hiện tại, đảm bảo rằng nó có phạm vi và Lifespan cần thiết cho hoạt động.

    Mong bài viết này sẽ giúp ích ít nhiều cho các bạn. Hẹn gặp lại mọi người trong bài viết tiếp theo.

  • 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.

  • IBInspectable and IBDesignable in Swift

    IBInspectable and IBDesignable in Swift

    Xin chào mọi người, bài viết này mình xin giới thiệu với các bạn về IBInspectable và IBDesignable trong swift.

    IBInspectable

    Khi các bạn thực hiện code UI bằng Interface builder của Xcode, nó sẽ hiển thị cho các bạn một số các thuộc tính cơ bản để các bạn có thể chỉnh sửa. Hình dưới đây là Atributes Inspector của UIView.

    Inspectable-and-IBDesignable

    Có bao giờ bạn muốn thêm các thuộc tính của một UI trong tab Attributes Inspector chưa? Nếu bạn có ý định này thì xin chúc mừng. IBInspectable sẽ giúp bạn làm được việc này.

    IBInspectable giúp cho bạn có thể thêm được rất nhiều các thuộc tính vào tab Attributes Inspector từ đó giúp các bạn dễ dàng chỉnh sửa nó trên Interface builder của Xcode một cách dễ dàng.

    Vậy để sử dụng IBInspectable thêm các thuộc tính vào Interface builder của xCode chúng ta làm như sau:

    Ở đây mình sẽ làm một ví dụ để thêm thuộc tính cho UIView

    Như bạn đã biết thì UIView trên Interface builder không có các thuộc tính như cornerRadius(bo góc), borderColor(màu viền), borderWidth(độ rộng viền)… Vậy trong ví dụ này mình sẽ thêm các thuộc tính này vào Attributes inspector của UIView.

    Đầu tiên mình tạo một class CommonView kế thừa lại UIView như sau:

    class CommonView: UIView {
        // thêm thuộc tính để bo góc cho View
        @IBInspectable
        var cornerRadius: CGFloat = 4 {
            didSet {
                clipsToBounds = true
                layer.cornerRadius = cornerRadius
            }
        }
        
        // thêm thuộc tính để đặt độ dày của viền cho View
        @IBInspectable
        var borderWidth: CGFloat = 1 {
            didSet {
                layer.borderWidth = borderWidth
            }
        }
        // thêm thuộc tính để sửa màu viền cho View
        @IBInspectable
        var borderColor: UIColor = .red {
            
            didSet {
                layer.borderColor = borderColor.cgColor
            }
        }
    }

    Để sử dụng CommonView thì chúng ta mở file Storyboard hoặc file xib lên và kéo một UIView vào, sau đó đổi class từ UIView(mặc định) sang CommonView, vậy là xong.

    Kết quả chúng ta sẽ được như sau:

    Inspectable-and-IBDesignable
    Các thuộc tính Corner Radius, Border Witdh, Border color đã được thêm vào Attributes Inspector băng thuộc tính @IBInspectable

    Vậy là chúng ta đã thêm được các thuộc tính vào Attributes Inspector của Xcode, tuy nhiên chúng ta cần phải build app lên thì mới thấy sự thay đổi. Sao nó không thay đổi ngay khi chúng ta sửa giá trị như các thuộc tính khác? Vì một mình IBInspectable thì không làm được vì vậy các nhà phát triển của Apple mới đẻ ra IBDesignable để làm việc này.

    IBDesignable

    IBDesignable cho phép chúng ta xem trực tiếp các thay đổi của view trong storyboard hoặc trong file xib mà không cần phải run ứng dụng.

    Để sử dụng IBDesignable thì chúng ta chỉ cần thêm @IBDesignable vào đằng trước class mà chúng ta muốn và override lại func prepareForInterfaceBuilder() để nó update giá trị và hiển thị lên trên Interface builder, trong ví dụ này mình để nó ở trước class CommonView của mình như sau:

    @IBDesignable
    class CommonView: UIView {
        // set giá trị để hiển thị cho Interface builder
        override func prepareForInterfaceBuilder() {
            setupView()
        }
        // setup view
        private func setupView() {
            self.layer.cornerRadius = cornerRadius
            self.layer.borderWidth = borderWidth
            self.layer.borderColor = borderColor.cgColor
        }
    
        @IBInspectable
        var cornerRadius: CGFloat = 4 {
            didSet {
                clipsToBounds = true
                layer.cornerRadius = cornerRadius
            }
        }
        
        @IBInspectable
        var borderWidth: CGFloat = 1 {
            didSet {
                layer.borderWidth = borderWidth
            }
        }
        
        @IBInspectable
        var borderColor: UIColor = .red {
            
            didSet {
                layer.borderColor = borderColor.cgColor
            }
        }
    }

    CHÚ Ý: Bạn cần phải override lại func prepareForInterfaceBuilder() và set lại các thuộc tính để nó có thể update giá trị cho interface builder.

    Bây giờ chúng ta chỉ cần kéo UIView vào là nó sẽ tự apply các thuộc tính và khi sửa tại Attributes inspector thì nó sẽ được update ngay mà không cần phải build ứng dụng để kiểm tra lại UI.

    Kết quả chúng ta được như hình dưới đây:

    IBInspectable and IBDesignable uiview

    Trong trường hợp các bạn muốn làm common và không cho sửa thuộc tính nào trên interface builder thì bạn chỉ cần bỏ IBInspectable của thuộc tính đó đi là được.

    Tổng kết

    Vậy là mình đã giới thiệu cho các bạn một phương pháp để thực hiện làm common rất hiệu quả và tiết kiệm thời gian khi làm ứng dụng di động trên iOS. Từ ví dụ common view này chúng ta có thể phát triên cho các common khác như UILabel, UIButton … Mình hi vọng bài viết sẽ giúp ích cho các bạn trong quá trình học hỏi và phát triển ứng dụng iOS.