Kiến trúc MVP trong Android với Dagger2 ,Retrofit

5
Dịch vụ dạy kèm gia sư lập trình

Bài viết này, mình sẽ hướng dẫn chi tiết cách sử dụng thư viện Dagger2 và Retrofit để tạo dự án theo kiến trúc MVP trong android thực sự clean, dễ maintain và mở rộng sau này.

Thế nào là dự án “clean”? Hay nói cách khác, thế nào là clean code? Bạn có thể tham khảo bài viết sau: Clean Code Android: Bạn đã thật sự hiểu đúng chưa?

Trên VNTALKING, mình đã từng chia sẻ câu chuyện về mô hình MVP trong Android. Các bạn có thể đọc lại để hiểu câu chuyện tại sao người ta lại nghĩ ra mô hình MVP cho Android.

Nội dung bài viết:

  • Tìm hiểu kỹ hơn nguyên tắc thiết kế của mô hình MVP
  • Cách triển khi mô hình MVP
  • Xây dựng cấu trúc thư mục/package trong dự án Android theo mô hình MVP
  • Tìm hiểu Dependency Injection là gì?
  • Cấu hình và sử dụng thư viện Dagger2, Retrofit để triển khai trong mô hình MVP

Chúng ta bắt đầu nhé!

#Nguyên tắc thiết kế của kiến trúc MVP trong android gồm những gì?

Model View Presenter(MVP) là một architectural pattern được thiết kế để tăng cường khả năng Unit testing và tăng tính độc lập giữa tầng dữ liệu và tầng hiển thị dữ liệu – View (một cải tiến so với mô hình MVC).

Minh họa kiến trúc MVP

Cụ thể trong lập trình Android, kiến trúc MVP sẽ giúp chúng ta tách tầng business logic ra khỏi lớp View (là các Activities/ Fragments/custom Views) thông qua tầng Presenter.

Về cụ thể nhiệm vụ của từng tầng View, Presenter, Model trong MVP thì mời các bạn đọc lại biết này của mình: Triển khai mô hình MVP cho lập trình ứng dụng Android

Mình chỉ lưu ý lại một điều: TầngPresenter không nên gọi và sử dụng Android API. Tầng này chỉ thuần xử lý business logic của ứng dụng.

#Cấu trúc thư mục dự án khi ứng dụng kiến trúc MVP trong Android

Dựa trên những nguyên tắc thiết kế của kiến trúc MVP, chúng ta sẽ phân loại thư mục dự án ứng dụng Android như sau:

  • data              Chứa các Data classses / POJO classes , thao tác với cơ sở dữ liệu.
  • di                  Chứa Dependency Injection Code kiểu như các components hay modules.
  • network        Tập hợp các class liên quan API calls, API End points / Retrofit Code
  • service          Nếu ứng dụng của bạn cần đến Service thì để ở đây
  • ui                  Tất cả các class liên quan đến UI như: Activities/Fragments/CustomView
  • utils
Cấu trúc thư mục trong dự án Android theo kiến trúc MVP
Cấu trúc thư mục trong dự án Android theo kiến trúc MVP

Ngoài ra, có một điều đặc biệt quan trọng trong cấu trúc này. Đó là mình sẽ thêm một BaseActivity, tất cả các Activity  trong dự án đều sẽ extend từ BaseActivity này.

BaseActivity sẽ có chức năng gì?

BaseActivity là một lớp trừu tượng(abstract class), mục đích cơ bản là đặt tất các đoạn mã xử lý chung cho mọi Activity.

Ví dụ như: code xử lý nút back, code cho ActionBar… Điều này sẽ giúp cho dự án không bị lặp code, code sẽ trở nên clean hơn.

Trong trường hợp bài viết này, BaseActivity được kế thừa trực tiếp từ AppCompatActivity và implememt một interface IView(mình sẽ giải thích sau về interface này).

Đây là đoạn mã của BaseActivity:

abstract class BaseActivity : AppCompatActivity(),IView {

    /**
     * A dialog showing a progress indicator and an optional text message or
     * view.
     */
    protected var mProgressDialog: ProgressDialog?=null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(setLayout())
        initialzeProgressDialoge()
        init(savedInstanceState)

    }

    fun initialzeProgressDialoge(){

        if(mProgressDialog==null) {

            mProgressDialog = ProgressDialog(this)
            mProgressDialog!!.isIndeterminate = true
            mProgressDialog!!.setCancelable(false)
        }

    }

    override fun onPostCreate(savedInstanceState: Bundle?) {
        super.onPostCreate(savedInstanceState)

    }

    override fun onResume() {
        super.onResume()
    }

    override fun onDestroy() {
        super.onDestroy()
        System.gc()
        System.runFinalization()
        dismissProgress()
        mProgressDialog=null
    }

    @LayoutRes
    abstract fun setLayout():Int
    abstract fun init(savedInstanceState: Bundle?)
    abstract fun onStartScreen()
    abstract fun stopScreen()


    fun showProgress(msgResId: Int,
                     keyListener: DialogInterface.OnKeyListener?) {
        if (isFinishing)
            return

        if (mProgressDialog!!.isShowing) {
            return
        }

        if (msgResId != 0) {
            mProgressDialog?.setMessage(resources.getString(msgResId))
        }

        if (keyListener != null) {
            mProgressDialog?.setOnKeyListener(keyListener)

        } else {
            mProgressDialog?.setCancelable(false)
        }
        mProgressDialog?.show()
    }

    /**
     * @param isCancel
     */
    fun setCancelableProgress(isCancel: Boolean) {
        if (mProgressDialog != null) {
            mProgressDialog?.setCancelable(true)
        }
    }

    /**
     * cancel progress dialog.
     */
    fun dismissProgress() {
        if (mProgressDialog != null && mProgressDialog!!.isShowing) {
            mProgressDialog?.dismiss()
        }
    }


    override fun hideLoading() {
        dismissProgress()
    }

    override fun showLoading() {
        showProgress(R.string.loading, null)
    }

    override fun loadError(e: Throwable) {
        showHttpError(e)
    }

    override fun loadError(msg: String) {
        Toast.makeText(this,msg,Toast.LENGTH_SHORT).show()
    }

    /*
    Improper handling in real case
     */

    protected fun showHttpError(e: Throwable) {
      loadError(e.localizedMessage)
    }

    override fun onStop() {
        super.onStop()
        stopScreen()
    }

}

BasePresenter cần tính năng gì?

Cũng giống như tầng View, khi có BaseActivity. Với tầng Presenter cũng vậy, chúng ta sẽ xây dựng một BasePresenter như sau:

open class Preseneter<V>(@Volatile var view: V? ){
    companion object {
        /*
        var compositeDisposables: CompositeDisposable
        Every method which will be part of presenter lyer will be added in it so we could dispose off them once they are no more in our use
        */
        var compositeDisposables: CompositeDisposable=CompositeDisposable()

    }

    init {

    }

    protected fun view(): V? {
        return view
    }

    @CallSuper
    fun unbindView() {
        if (compositeDisposables != null) {
            compositeDisposables.clear()
            compositeDisposables.dispose()
        }
        this.view = null
    }

    fun addDisposable(disposable: Disposable) {
        compositeDisposables.add(disposable)
    }
}

Trong kiến trúc MVP thì vấn đề memory leak rất hay xảy ra nếu bạn xử lý không tốt. Nguyên nhân phổ biến nhất là liên quan đến các việc kết nối mạng.

Ví dụ các ứng dụng cần kết nối server: Đọc báo, đọc truyện, backup cloud… khi mà ứng dụng cần kết nối tới server để lấy bài. Có rất nhiều trường hợp mạng chậm, mạng disconnect giữa chừng, server bị die, activity bị destroy khi đang kết nối…

Tất cả các trường hợp đó nếu bạn xử lý không tốt thì ứng dụng khả năng bị memory leak rất cao.

Trong đoạn code của BasePresenter, mình đã xử lý trường hợp khi View bị destroy và đặt view đó thành NULL để GC có thể clear nó.

#Xây dựng ứng dụng Android sử dụng kiến trúc MVP

Dựa trên nguyên tắc của MVP và phần source code cơ bản mà mình đã giới thiệu ở trên. Chúng ta sẽ cùng triển khai kiến trúc MVP cho một bài toán cụ thể.

Ở đây mình lấy ví dụ: Chúng ta sẽ xây dựng ứng dụng có tính năng login. Đây là tính năng rất phổ biến ở ứng dụng Android.

Đầu tiên, chúng ta tạo một LoginActivity và đặt vào UI package như bên dưới:

mvp-login-logic
MVP Android Login Code

Trong đó: Lớp LoginPresenter sẽ xử lý logic xác thực việc nhập username/password của người dùng.

– Nếu Username/Password không hợp lệ, LoginPresenter sẽ gọi một hàm của LoginView để hiển thị thông báo error cho người dùng.

– Nếu Username/Password hợp lệ, LoginPresenter sẽ gọi một Login API tới server và trả kết quả lên View(LoginActivity)

interface LoginPresenter {

    fun peformLogin(userName: String, userPassword: String)

    fun validateUser(userName: String, userPassword: String)
}
interface LoginView {

    fun navigateToHome()

    fun onBackPress()

    fun onPasswordError()
}
// LoginActivity will implement LoginView Interface.

class LoginActivity : AppCompatActivity(), LoginView {

    lateinit var loginPresenter: LoginPresenter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        // Initialize Presenter Implementation in onCreate.
        // And pass it reference to the View.
        // So Presenter can call View (LoginActivity) methods.

        loginPresenter = LoginPresenterImpl(this)
        loginPresenter.validateUser("hammad", "")

    }

    override fun onPasswordError() {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun onBackPress() {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun navigateToHome() {
       // START ACTIVITY INTENT CODE GOES HERE.
    }
}

Như các bạn thấy trong đoạn code trên, LoginPresenterImpl được implements từ LoginPresenter Interface.

Và nó nhận reference của LoginView thông qua hàm khởi tạo(Contructor). Mục đích chúng ta truyền LoginView vào LoginPresenterImpl là để tầng Presenter có thể gọi hàm và update kết quả lên View.

class LoginPresenterImpl(var loginViewInit: LoginView) : LoginPresenter {

    override fun validateUser(userName: String, userPassword: String) {
       
        // This function inside presenter layer
       // will have username and password validation logic.
       
      if (userPassword=="")
        loginViewInit.onPasswordError() 
 }

 override fun peformLogin(userName: String, userPassword: String) {
      
      if (userName == "hammad") {
         loginViewInit.navigateToHome()
      }
 }
}

Như vậy, chúng ta đã hoàn thành xong việc xây dựng bộ khung code cho kiến trúc MVP.

Phần tiếp theo, mình sẽ tiếp tục triển khai thư viện Dagger2 và và Retrofit để thực hiện tương tác với server qua hàm performLogin() trong tầng Presenter.

>> Xem ngay: Presenter trong Android

#Dependency Injection (DI) là gì?

Dependeny Injection là 1 kỹ thuật, 1 design pattern được sử dụng để cố gắng giảm sự phụ thuộc giữa các object với nhau khi chúng có mối quan hệ phụ thuộc giữa một object này với một object khác.

Đọc xong định nghĩa trên mà bạn vẫn không hiểu gì phải không? Chuyện bình thường trên huyện ấy mà 🙂

Để mình lấy ví dụ: Dự án của bạn có 2 class: ClassA, ClassB, ClassC.

Trong đó ClassC cần một đối tượng của ClassA và ClassB.

Trong khi ClassB cần một đối tượng của ClassA.

ClassA {
    private float md5;
    private String key;
}
Class C {
    private String mName;
    private A mObjectA;
    private B mObjectB;
}
ClassB {
    private int mId;
    private A mObjectA;
}
Ví dụ về Dependency Injection
Ví dụ về Dependency Injection

Điều này có nghĩa là ClassC phụ thuộc vào ClassB và ClassA. Và ClassB phụ thuộc vào ClassA. Chính vì sự rằng buộc này mà ứng dụng rất dễ bị Memory Leak.

Để bỏ được sự rằng buộc này, chúng ta phải cung cấp phương thức để tạo đối tượng ClassA và ClassB cho ClassC.

Đây là lúc chúng ta sử dụng thư viện Dagger2.

Dagger2 Dependency Injection (DI) là gì?

Dagger2 là một framework được sử dụng để quản lý các dependencies. Dagger2 thường sử dụng các annotations.

Có một số loại annotations trong Dagger2 như:

  • @Provides
  • @Module
  • @Component
  • @Inject

Để tìm hiểu kỹ hơn về từng loại annotations của Dagger2, mình sẽ dành một bài viết riêng về nó sau. Các bạn nhớ đón đọc nhé.

#Cài đặt Dagger2 và Retrofit trong Android Studio

Các bạn add thêm dependencies vào build.gradle của ứng dụng như sau:

//RETROFIT ...  NETWORK LIBRARY

implementation 'com.squareup.retrofit2:retrofit:2.4.0'
// RETROFIT .. CONVERTER
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'

//  Dagger2 required Gradle dependencies.

implementation 'com.google.dagger:dagger-android:2.15'
implementation 'com.google.dagger:dagger-android-support:2.15'

#Cách triển khai Dagger2 trong kiến trúc MVP trong Android

Quay trở lại project  mà mình triển khai kiến trúc MVP ở trên. Các bạn thấy mình có tạo một folder/package có tên là di.

Chúng ta sẽ tạo các component và module liên quan Dagger2 trong package này.

Đầu tiên, mình sẽ tạo 2 packages: Component và Module bên trong di package.

Sau đó tạo một Interface có tên là ApplicationComponent trong Component package

@Component(modules = [NetModule::class])
interface ApplicationComponent {
    fun inject(mLoginPresenterImpl: LoginPresenterImpl)
}

Tiếp theo tạo một lớp NetModule bên trong module package.

@Module
class NetModule {

    @Provides
    fun provideRetrofit(gson: Gson): Retrofit {
        return Retrofit.Builder().addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .baseUrl("https://jsonplaceholder.typicode.com/").build()
    }

    @Provides
    fun providesGson(): Gson {
        return GsonBuilder().create()
    }

    @Provides
    fun provideNetworkService(retrofit: Retrofit): INetworkApi {
        return retrofit.create(INetworkApi::class.java)
    }
}

Bây giờ chúng ta sẽ khởi tạo ApplicationComponent interface để cài đặt các dependencies cần thiết. Việc này chúng ta sẽ thực hiện trong lớp Application.

open class ApplicationClass : Application() {


    public lateinit var applicationComponent: ApplicationComponent

    override fun onCreate() {
        super.onCreate()

        // ApplicationComponent is our component interface.
        // NetModule is our Module class.

        applicationComponent = DaggerApplicationComponent.builder()
                .netModule(NetModule())
                .build()

        applicationComponent.inject(this)
    }
}

Ở bước này, nếu Android Studio thông báo lỗi, thì bạn đơn giản chỉ cần vào build -> Re-Build để sửa lỗi.

#Cách triển khai Retrofit trong kiến trúc MVP

Để sử dụng thư viện Retrofit, bạn đừng quên thêm INTERNET permission trong manifest file nhé.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.es.developine">
    <uses-permission android:name="android.permission.INTERNET" />

Trong dự án mình họa của bài viết này, mình sẽ sử dụng Retrofit kết nối tới API có URL như sau:

https://jsonplaceholder.typicode.com/posts

Các bước thực hiện như sau:

  1. Tạo  interface INetworkApi trong network package.
  2. Tạo một lớp EndPoints trong network package.
//INetworkApi.kt
interface INetworkApi {
    @GET(Endpoints.posts)
    fun getAllPosts(): Observable<List<PostData>>
}
//EndPoints.kt
object Endpoints {
    const val posts = "posts/"
}
retrofit_recyclerview_kotlin_mvp_android
Tạo thêm Post package

Trong PostActivity, chúng ta sẽ gọi API network sử dụng Retrofit2.

Retrofit sẽ tự động parse JSON được server trả về thông qua thư viện Gson.

Bạn update lại DaggerApplicationComponent.kt như sau:

@Component(modules = [AppModule::class, NetModule::class])
interface ApplicationComponent {

    fun inject(mewApplication: ApplicationClass)
    fun inject(mLoginPresenterImpl: LoginPresenterImpl)
    fun inject(mLoginActivity: LoginActivity)
    fun inject(mPostPresenterImpl: PostPresenterImpl)

}

Tạo lớp Data: PostData.kt (Mục đích để Gson có thể mapping và tự động parse JSON).

data class PostData(
      @SerializedName("userId") val userId: Int,
      @SerializedName("id") val id: Int,
      @SerializedName("title") val title: String,
      @SerializedName("body") val body: String
)

Và một interface PostView.kt

interface PostView {

    fun showAllPosts(postList: List<PostData>)
}

PostPresenter.kt

// tạo một interface PostPresenter.kt
interface PostPresenter {
    fun getAllPosts()
}

Cuối cùng PostActivity của chúng ta sẽ như sau:

class PostActivity : AppCompatActivity(), PostView {


    lateinit var postPresenter: PostPresenter


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_post)


        postPresenter = PostPresenterImpl(this, application)

        postPresenter.getAllPosts()
    }


    override fun showAllPosts(postList: List<PostData>) {

        Log.d("Response", "" + postList)
    }

}

Còn lớp PostPresenterImpl.kt

class PostPresenterImpl(var postView: PostView, var applicationComponent: Application) : PostPresenter {
    @Inject
    lateinit var mNetworkApi: INetworkApi

    init {
        (applicationComponent as ApplicationClass).applicationComponent.inject(this)
    }

    override fun getAllPosts() {

        var allPosts = mNetworkApi.getAllPosts()
        allPosts.subscribeOn(IoScheduler()).observeOn(AndroidSchedulers.mainThread())
                .subscribe {
                    postView.showAllPosts(it)
                }
    }
}

#Tạm kết

Như vậy, mình đã hướng dẫn chi tiết cách triển khai kiến trúc MVP trong Android. Trong đó, mình sử dụng Dagger2 và Retrofit để minh họa cho tính năng login của ứng dụng.

Mặc dù bài viết hơi dài chút xíu, các bạn chịu khó đọc nhé.

Nếu đọc bài viết vẫn còn thắc mắc thì các bạn có thể download toàn bộ source của bài hướng dẫn tại đây:

Hi vọng rằng, qua bài viết này các bạn sẽ hiểu về kiến trúc MVP trong android hơn, dự án cũng sẽ clean code hơn.

Đừng quên chia sẻ bài viết để ủng hộ mình nhé.

Dịch vụ phát triển ứng dụng mobile giá rẻ - chất lượng
Bài trướcTỷ lệ chuyển đổi cài đặt app tăng bằng cách giảm APK size
Bài tiếp theoMiễn phí mã nguồn Flashlight 3D Android trị giá 69$
Tên đầy đủ là Dương Anh Sơn. Tốt nghiệp ĐH Bách Khoa Hà Nội. Mình bắt đầu nghiệp coder khi mà ra trường chẳng xin được việc đúng chuyên ngành. Mình tin rằng chỉ có chia sẻ kiến thức mới là cách học tập nhanh nhất. Các bạn góp ý bài viết của mình bằng cách comment bên dưới nhé !

5
Bình luận. Cùng nhau thảo luận nhé!

avatar
  Theo dõi bình luận  
Mới nhất Cũ nhất Nhiều voted nhất
Thông báo
Lữ Hồ
Guest
Lữ Hồ

sao không tìm thấy component DaggerApplicationComponent này a nhỉ.

Ryo Kami
Member
Ryo Kami

Bro, cho e hỏi là nếu mình dùng trong fragment thì thằng application import thế nào ạ