MVP stands for Model-View-Presenter. MVP is an architectural pattern whose main purpose is to achieve separation of concerns through a clear distinction between the roles of each of its layers:
Dependencies
Start by opening build.gradle in your app directory. Add the following line inside the dependencies block:
# apply plugin: 'kotlin-kapt' def cardviewVersion = "1.0.0" implementation "androidx.cardview:cardview:$cardviewVersion" def constraintLayoutVersion = "2.0.0-beta3" implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion" implementation 'com.squareup.retrofit2:converter-gson:2.6.0' implementation 'androidx.recyclerview:recyclerview:1.1.0-rc01' def roomVersion = "1.0.0" implementation "android.arch.persistence.room:runtime:$roomVersion" kapt "android.arch.persistence.room:compiler:$roomVersion" def lifecycle_version = '2.1.0' implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
AndroidManifest.xml
<application ...>
<activity android:name=".main.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
main directory
File /main/MainActivity.kt
class MainActivity: AppCompatActivity(), MainContract.ViewInterface {
private lateinit var mainPresenter: MainContract.PresenterInterface
private lateinit var adapter: MainAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupPresenter()
setupViews()
}
private fun setupViews() {
rvItems.layoutManager = LinearLayoutManager(this)
adapter = MainAdapter(mutableListOf())
rvItems.adapter = adapter
}
private fun setupPresenter() {
val dataSource = LocalDataSource(application)
mainPresenter = MainPresenter(this, dataSource)
mainPresenter.getPostsList()
}
override fun displayItems(items: List<Post>) {
adapter.setPosts(items)
rvItems.visibility = View.VISIBLE
tvNoItems.visibility = View.GONE
}
override fun displayNoItems() {
rvItems.visibility = View.GONE
tvNoItems.visibility = View.VISIBLE
}
override fun displayMessage(message: String) {
Toast.makeText(this@MainActivity, message, Toast.LENGTH_LONG).show()
}
override fun displayError(message: String) {
displayMessage(message)
}
}
File activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".main.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvItems"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true" />
<TextView
android:id="@+id/tvNoItems"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="No items!" />
</RelativeLayout>
File /main/MainContract.kt
class MainContract {
interface PresenterInterface {
fun getPostsList()
}
interface ViewInterface: LifecycleOwner {
fun displayItems(movieList: List<Post>)
fun displayNoItems()
fun displayMessage(message: String)
fun displayError(message: String)
}
}
File /main/MainPresenter.kt
class MainPresenter(private var viewInterface: MainContract.ViewInterface,
private var dataSource: LocalDataSource): MainContract.PresenterInterface {
private val TAG = "MainPresenter"
private var allPosts: LiveData<List<Post>> = dataSource.allPosts
override fun getPostsList() {
allPosts.observe(viewInterface, Observer { posts ->
// for testing
if (posts.isEmpty()) {
dataSource.insert(Post(userId = 1, title = "Post 1", text = "Body 1"))
dataSource.insert(Post(userId = 1, title = "Post 2", text = "Body 2"))
}
if (posts.isEmpty()) {
viewInterface.displayNoItems()
} else {
viewInterface.displayItems(posts)
}
})
}
}
File /main/MainAdapter.kt
class MainAdapter(private val posts: MutableList<Post>)
: RecyclerView.Adapter<MainAdapter.PostHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_post_main, parent, false)
return PostHolder(view)
}
override fun getItemCount(): Int = posts.size ?: 0
override fun onBindViewHolder(holder: PostHolder, position: Int) {
holder.bind(posts[position])
}
fun setPosts(items: List<Post>) {
this.posts.clear()
this.posts.addAll(items)
notifyDataSetChanged()
}
inner class PostHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun bind(post: Post) = with(view) {
tvTitle.text = post.title
tvBody.text = post.text
}
}
}
File item_post_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_margin="8dp"
app:cardCornerRadius="6dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="10dp">
<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Post Title" />
<TextView
android:id="@+id/tvBody"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvTitle"
tools:text="Post Body" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
model directory
File /model/Post.kt
@Entity
data class Post (
@PrimaryKey(autoGenerate = true)
var id: Int? = null,
var userId: Int = 0,
var title: String = "",
@SerializedName("body")
var text: String = ""
)
File /model/PostDao.kt
@Dao
interface PostDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(post: Post)
@Query("select * from Post")
fun getAll(): LiveData<List<Post>>
@Update
fun update(post: Post)
@Query("DELETE FROM Post WHERE id = :id")
fun delete(id: Int?)
}
File /model/PostDatabase.kt
@Database(entities = [Post::class], version = 1)
abstract class PostDatabase : RoomDatabase() {
abstract fun postDao(): PostDao
companion object {
private val lock = Any()
private const val DB_NAME = "PostDatabase"
private var INSTANCE: PostDatabase? = null
fun getInstance(application: Application): PostDatabase {
synchronized(PostDatabase.lock) {
if (PostDatabase.INSTANCE == null) {
PostDatabase.INSTANCE =
Room.databaseBuilder(application, PostDatabase::class.java, PostDatabase.DB_NAME)
.build()
}
}
return INSTANCE!!
}
}
}
File /model/LocalDataSource.kt
open class LocalDataSource(application: Application) {
private val postDao: PostDao
open val allPosts: LiveData<List<Post>>
init {
val db = PostDatabase.getInstance(application)
postDao = db.postDao()
allPosts = postDao.getAll()
}
fun insert(post: Post) {
thread {
postDao.insert(post)
}
}
}
net directory
File /net/PostApi.kt
interface PostApi {}
File /net/RetrofitClient.kt
class RetrofitClient {}