การวางโครงสร้าง Android App แบบ MVP (Register Example)

บทความนี้ไม่เน้นความสวยงามเน้อ เน้นความเข้าใจโครงสร้างและโฟว์การทำงานของ MVP ครับ #For dev


ติดตั้ง dependencies

build.gradle (Module : app)

// RETROFIT ...  NETWORK LIBRARY
implementation 'com.squareup.retrofit2:retrofit:2.4.0'

// RETROFIT .. CONVERTER & ADAPTER
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'

// RX-ANDROID

implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'



วาง Layout แบบนู๊ปๆ >_<

activity_main.xml  
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/edtUsername"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="username :"
        android:inputType="text" />

    <EditText
        android:id="@+id/edtPassword"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="password : "
        android:inputType="textPassword" />

    <Button
        android:id="@+id/btnRegister"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Register" />
</LinearLayout>



MainActivity.kt

class MainActivity : AppCompatActivity(), MainActivityPresenter.View {

    private var presenter: MainActivityPresenter? = null
    private var progressBar: ProgressBar? = null

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

        presenter = MainActivityPresenter(this)

        val edtUsername = findViewById<EditText>(R.id.edtUsername)
        val edtPassword = findViewById<EditText>(R.id.edtPassword)

        // setup Progress bar
        initProgressBar()

        // Update ตัวแปร username , password แบบทีละตัวอักษร

        edtUsername.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
            override fun onTextChanged(char: CharSequence, start: Int, before: Int, count: Int) {
                presenter!!.updateUsername(char.toString())   // เรียกใช้ method updateUsername ใน presenter
            }

            override fun afterTextChanged(s: Editable) {
                hideProgressBar()
            }
        })

        edtPassword.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
            override fun onTextChanged(char: CharSequence, start: Int, before: Int, count: Int) {
                presenter!!.updatePassword(char.toString())  // เรียกใช้ method updatePassword ใน presenter
            }

            override fun afterTextChanged(s: Editable) {
                hideProgressBar()
            }
        })

        // คลิก Register
        btnRegister.setOnClickListener {
            presenter!!.submitRegister()   // เรียกใช้ method submitRegisterใน presenter
        }
    }

    private fun initProgressBar() {
        progressBar = ProgressBar(this, null, android.R.attr.progressBarStyleSmall)
        progressBar!!.isIndeterminate = true
        val params = RelativeLayout.LayoutParams(Resources.getSystem().displayMetrics.widthPixels, 250)
        params.addRule(RelativeLayout.CENTER_IN_PARENT)
        this.addContentView(progressBar, params)
        showProgressBar()
    }

    // รอเรียกใช้จากฝั่ง presenter
    override fun showProgressBar() {
        progressBar!!.visibility = View.VISIBLE
    }

    override fun hideProgressBar() {
        progressBar!!.visibility = View.INVISIBLE
    }

}


MainActivityPresenter.kt

class MainActivityPresenter(private val view: View) {

    private val user: User = User()  // แยก Model User สำหรับการทำงานในโมดูล User

    fun updateUsername(username: String) {
        user.username= username
    }

    fun updatPassword(password: String) {
        user.password= password

    }

    fun submitRegister(){
       val response = user.register()     << Response โผล่นี่
        ..
        ..
        // สั่งอัพเดท View อะไร บลาๆ แล้วแต่ชอบเลยครับ
    }


    // ตัวอย่างการใช้ Interface ส่งข้อมูลกลับไปที่ MainActivity.kt
    interface View {
        fun showProgressBar()
        fun hideProgressBar()
    }
}



User.kt

class User {

    var username = ""
    var password = ""


    fun getUserProfile(): String {
        return "Username: $usrename\n Password : $password"
    }

    fun register() : Boolean{
        val post = PostWithBody(username, password) // Format ก้อนข้อมูลที่จะส่งออก
        val retrofit = Retrofit.Builder().addConverterFactory(
            GsonConverterFactory.create(GsonBuilder().create()))
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .baseUrl("https://jsonplaceholder.typicode.com/").build()

        val userApi = retrofit.create(UserApi::class.java)
        val response = userApi.registerMember(post)

        response.observeOn(AndroidSchedulers.mainThread()).subscribeOn(IoScheduler()).subscribe {

            Log.e("TEST:", it.toString())
            ..
            ..
            ..
            // ไปต่อกันเองเน้อ
        }
        return true
    }

    ..
    ..
    ..
    // ตรงนี้อาจจะมีฟังก์ชั่น Login() มา เพิ่มเติมได้นะ

}



Interface ของ Retrofit
UserApi.kt


interface UserApi {

    @POST("/posts") // ส่งข้อมูลแบบ Post Request ไปที่ https://jsonplaceholder.typicode.com/posts

    fun registerMember(@Body data: PostWithBody): Observable<RegisterResponse // ข้อมูลที่ส่งออก , ข้อมูลที่รับเข้า

}


Format ของ Object ที่ใช้ รับ, ส่งกับ Server
ถ้าทำเป็นทีมก็ ตะโกนถาม Backend  เอ้ยๆ อันนี้ๆๆๆๆ ปะวะ ( แล้วก็ตั้งผิดอยู่ดี >_<" )

PostWithBody.kt

data class PostWithBody(
    @SerializedName("username") val username: String,  // ทำไมต้องใส่ SerializedName 
    @SerializedName("password") val password: String
)

RegisterResponse.kt

data class RegisterResponse(
    @SerializedName("response") val response: String
     ..
     ..
)