5 sai lầm phổ biến trong lập trình Android

21 Tháng Mười Hai, 2022


Việc tối ưu hóa hiệu năng, hạn chế các lỗi và tăng cường bảo mật là một trong những yếu tố quan trọng nhất trong quá trình phát triển các ứng dụng Android. Tuy nhiên, nhiều lập trình viên Android vẫn thường hay mắc phải những lỗi khá phổ biến. Điều này có thể khiến app dễ bị giật lag hoặc nặng hơn là crash, gây ra trải nghiệm xấu cho người dùng.

Ngoài ra, nếu không bảo mật đúng cách, app của bạn cũng có thể bị hacker tấn công, lấy dữ liệu người dùng, đánh cắp tài khoản quan trọng, … Nếu app của bạn là app lớn với hàng triệu người dùng, thì việc giảm thiểu các vấn đề này sẽ giúp bạn hạn chế được các tổn thất về doanh thu. Điều quan trọng là chúng ta phải xác định những sai lầm này và thực hiện các biện pháp khắc phục để cải thiện điều đó.Vì vậy, trong bài viết này sẽ liệt kê 5 sai lầm phổ biến trong lập trình Android và cách khắc phục.

1. Cập nhật View của Fragment không đúng cách

Nếu bạn có đoạn code như sau (collect Coroutines Flow/subscribe RxJava Observable để cập nhật UI):

class MyViewModel : ViewModel() {
  val myStateFlow: StateFlow<String>
  val myObservable: Observable<String>
}
class MyFragment : Fragment() {
  val compositeDisposable = CompositeDisposable()
  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    viewModel.myStateFlow
      .onEach { binding.textView1.text = it }
      .launchIn(viewLifecycleOwner.lifecycleScope)
    viewModel.myObservable
      .subscribe { binding.textView2.text = it }
      .addTo(compositeDisposable)
  }
  override fun onDestroyView() {
    compositeDisposable.clear()
    super.onDestroyView()
  }
}

Vấn đề của đoạn code ở trên là việc cập nhật UI và truy cập vào View của Fragment không được đúng cách. Khi app đi vào trạng thái background, việc giữ tham chiếu tới View và cập nhật View, sẽ dễ gây ra memory leak và tốn nhiều tài nguyên hơn (cả CPU và memory).

Giải pháp là chúng ta chỉ nên cập nhật UI khi View của Fragment đang hiển thị trên màn hình (ở trạng thái foreground), với lifecycle tương ứng là từ onStart cho đến onStop, hoặc sử dụng AndroidX Lifecycle.repeatOnLifecycle

class MyFragment : Fragment() {
  private var job: Job? = null
  private var disposable: Disposable? = null

  override fun onStart() {
    super.onStart()

    job = viewModel.myStateFlow
      .onEach { binding.textView1.text = it }
      .launchIn(viewLifecycleOwner.lifecycleScope)

    disposable = viewModel.myObservable
      .subscribe { binding.textView2.text = it }
  }

  override fun onStop() {
    job?.cancel()
    job = null

    disposable?.dispose()
    disposable = null
    
    super.onStop()
  }
}

2. Không encrypt các dữ liệu quan trọng

Việc lưu trữ các dữ liệu quan trọng (như JWT Token, Secret key, User info, …) trong SharedPreferences, Local databases hoặc Local files có thể là một rủi ro. Điều này khiến các dữ liệu đó có thể bị hacker tấn công một cách dễ dàng. Nếu một ai đó có thể truy cập vào các dữ liệu này, thì sẽ có thể giả mạo người dùng và thực hiện các hành động không mong muốn, ví dụ như sử dụng JWT Token để thực hiện các API requests. Nhiều lập trình viên Android không nhận thức được điều này và lưu dữ liệu quan trọng mà không encrypt trước khi lưu.

Giải pháp là sử dụng EncryptedSharedPreferences, hoặc mã hóa dữ liệu trước khi lưu vào storage (sử dụng AES hoặc sử dụng thư viện Google Tink).

val encryptedPrefs = EncryptedSharedPreferences.create(
  "secret_shared_prefs",
  MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
  context,
  EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
  EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

// use the shared preferences and editor as you normally would
encryptedPrefs.edit()
  .putString("key", "value")
  .apply()

3. Tham chiếu tới View/Fragment/Activity bằng các biến static/toàn cục.

Nếu bạn có đoạn code lưu các View/Fragment/Activity vào các biến static/toàn cục, để có thể dùng chung dữ liệu, giao tiếp giữa chúng với nhau, thì đó là một sai lầm.

class Holder {
  companion object {
    var myFragment: MyFragment? = null
    var myActivity: MyActivity? = null
    var myView: View? = null
  }
}

// in MyActivity class
Holder.myActivity = this

// in other activity class
Holder.myActivity?.getData()

Điều này dễ dẫn đến memory leak, vì các View/Fragment/Activity có khả năng sẽ không bao giờ được giải phóng. Ngoài ra, điều này cũng làm cho code khó đọc và khó bảo trì hơn. Bạn hoàn toàn có thể tránh memory leak bằng việc clear reference tới các View/Fragment/Activity khi chúng không còn được sử dụng.

class MyActivity : Activity() {
  override fun onDestroy() {
    Holder.myActivity = null
    super.onDestroy()
  }
}

Tuy nhiên, chúng ta có thể sử dụng các cách khác tốt hơn để đạt được mục đích tương tự, như là:

  • Sử dụng shared ViewModel và các Observable APIs như LiveData/RxJava Observable/Coroutines Flow để giao tiếp giữa các View/Fragment/Activity.
  • Sử dụng Activity Result / Fragment Result API.
  • Sử dụng Intent.putExtra/Fragment.setArguments(Bundle?) để truyền các giá trị ban đầu vào Activity/Fragment.

4. Không sử dụng SavedStateHandle trong ViewModel để lưu/khôi phục các State khi có Process Death.

Nếu bạn có đoạn code như sau:

class SearchViewModel : ViewModel() {
  private val _searchQuery = MutableStateFlow<String?>(null)
  val searchQuery: StateFlow<String?> = _searchQuery.asStateFlow()
  
  init {
    // do something with searchQuery
  }

  internal fun onSearchQueryChanged(query: String) {
    _searchQuery.value = query
  }
}

Nếu xảy ra Process Death, bạn sẽ không thể không phục lại search query được. Điều này gây ra trải nghiệm không tốt cho người dùng, vì họ sẽ phải nhập lại search query mà họ đã nhập trước đó, để có thể tìm kiếm lại.

Giải pháp là sử dụng SavedStateHandle, để lưu/khôi phục các State cần thiết. Chúng ta chỉ nên lưu những gì thực sự cần thiết, không phải tất cả mọi thứ. Phần còn lại nên được load từ Remove data source/Local data source. Ví dụ một số State nên được lưu bằng SavedStateHandle: search query, selected category, form data, …

5. Không tối ưu hóa việc thực hiện các API requests.

Ngày nay, các ứng dụng Android cần phải hiển thị dữ liệu một cách linh hoạt và luôn có sẵn.

Người dùng mong đợi trải nghiệm giao diện người dùng của họ không bao giờ bị chặn bởi các lần tải dữ liệu mới.

Cho dù ứng dụng là mạng xã hội, tin tức hay business-to-business, người dùng luôn mong đợi một trải nghiệm liền mạch cả khi trực tuyến và ngoại tuyến. Các ứng dụng cần phải sử dụng băng thông mạng hiệu quả và không làm chậm trễ các hoạt động của người dùng.Để đạt được điều này, chúng ta cần phải tối ưu hóa việc thực hiện các API requests, thông qua việc share và cache data ở in-memory và on-disk, throttle các requests để ngăn chặn việc thực hiện quá nhiều requests và ghi xuống disk cache. Trong Android, chúng ta có thể sử dụng các thư viện sau cho việc cache và share data:

Để throttle các requests và việc ghi xuống disk cache, chúng ta có thể sử dụng:

Tuy nhiên, việc kết hợp các thư viện này với nhau để đạt được kết quả mong muốn là một việc khá khó khăn, đòi hỏi nhiều thời gian và kiến thức. Để dễ dàng hơn, chúng ta có thể sử dụng thư viện Dropbox Store – một thư viện Kotlin để load dữ liệu từ remote và local data source. Thư viện cung cấp nhiều tùy chọn để lấy dữ liệu, ví dụ như lấy dữ liệu từ cache hoặc bỏ qua cache, chỉ thực hiện các requests hoặc chỉ lấy từ cache,vừa thực hiện các requests vừa lấy từ cache, …đồng thời nó cũng hỗ trợ việc throttle các requests.

Tạo một Store với Retrofit, Room Database và In-memory cache:
// EXAMPLE SOURCE CODE FROM https://github.com/MobileNativeFoundation/Store

StoreBuilder
    .from(
        // fetcher used to fetch data from network
        fetcher = Fetcher.of { key: String ->
            api.fetchSubreddit(key, "10")
                .data
                .children
                .map(::toPosts)
        },
        // source of truth used to cache data in disk (Disk as Single Source of Truth)
        sourceOfTruth = SourceOfTruth.of(
            reader = db.postDao()::loadPosts,
            writer = db.postDao()::insertPosts,
            delete = db.postDao()::clearFeed,
            deleteAll = db.postDao()::clearAllFeeds
        )
    ).cachePolicy(
        // in-memory cache policy
        MemoryPolicy.builder<Any, Any>()
            .setMaxSize(10)
            .setExpireAfterAccess(10.minutes)
            .build()
    ).build()

Sử dụng Store, chúng ta có thể lấy dữ liệu từ remote source, in-memory cache và disk cache một cách dễ dàng:

// EXAMPLE SOURCE CODE FROM https://github.com/MobileNativeFoundation/Store

// will get cached value followed by any fresh values,
// refresh will also trigger network call if set to `true` even if the data is available in cache or disk.
store
    .stream(StoreRequest.cached("3", refresh = false)) 
    .collect {}
 
// skip cache, go directly to fetcher
store
    .stream(StoreRequest.fresh(3))
    .collect {}

// display data to UI
viewModelScope.launch {
  // will get cached value followed by any fresh values from network
  store.stream(StoreRequest.cached(key = key, refresh = true)).collect { response ->
    when(response) {
        is StoreResponse.Loading -> showLoadingSpinner()
        is StoreResponse.Data -> {
            if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
            updateUI(response.value)
        }
        is StoreResponse.Error -> {
            if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
            showError(response.error)
        }
    }
  }
}

Kết luận

Chúng ta vừa tìm hiểu 5 sai lầm phổ biến trong lập trình Android và một số cách để khắc phục chúng.Hy vọng bài viết này sẽ giúp ích cho các bạn trong việc phát triển ứng dụng Android, giúp giảm thiểu các lỗi và tối ưu hóa hiệu năng của các ứng dụng.

  • android
  • Công nghệ thông tin

avatar image

Talenten

Những bài viết khác

Xem thêm double-arrow