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 {}