VIPER stands for View, Interactor, Presenter, Entity and Router. VIPER 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. VIPER is similar to the MVP architecture but adds another two layers of abstraction to provide the highest level of modularity. This five-layer structure aims to balance the workload between the different entities to provide the maximum level of modularity.
The role of each of the Entities are as follows:
The project contains the following packages:
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 android:name=".App" ...> <activity android:name=".view.activity.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application>
root directory
File App.kt
lateinit var db: PostDatabase class App: Application() { companion object { lateinit var INSTANCE: App } init { INSTANCE = this } override fun onCreate() { super.onCreate() db = PostDatabase.getInstance(this) INSTANCE = this } }
data directory
File /data/Interactor.kt
interface Interactor { fun getData(): PostState fun addPost(post: Post) }
File /data/PostInteractor.kt
class PostInteractor: Interactor { private val retrofitClient = RetrofitClient() private val postDao = db.postDao() override fun getData(): PostState { var state: PostState = PostState.LoadingState if (postDao == null) state = PostState.ErrorState("Error") else { state = PostState.LiveDataState(postDao.getAll()) } return state } override fun addPost(post: Post) { thread { postDao.insert(post) } } }
File /data/db/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 /data/db/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 /data/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 /data/model/PostResponse.kt
data class PostResponse ( var id: Int = 0, var userId: Int = 0, var title: String = "", @SerializedName("body") var text: String = "" )
File /data/net/PostApi.kt
interface PostApi {}
File /data/net/RetrofitClient.kt
class RetrofitClient {}
domain directory
File /domain/PostState.kt
sealed class PostState { object LoadingState: PostState() data class LiveDataState(val data: LiveData<List<Post>>): PostState() data class DataState(val data: List<Post>): PostState() data class ErrorState(val data: String): PostState() }
presenter directory
File /presenter/MainPresenter.kt
class MainPresenter(private val postInteractor: PostInteractor) { private lateinit var view: MainView fun bind(view: MainView) { this.view = view displayPosts() } private fun displayPosts() { val state = postInteractor.getData() when(state) { is PostState.LoadingState -> view.render(state) is PostState.ErrorState -> view.render(state) is PostState.LiveDataState -> { state.data.observe(view, Observer { posts -> // for testing if (posts.isEmpty()) { postInteractor.addPost( Post(userId = 1, title = "Post 1", text = "Body 1")) postInteractor.addPost( Post(userId = 1, title = "Post 2", text = "Body 2")) } view.render(PostState.DataState(posts)) }) Timer().schedule(5000) { postInteractor.addPost( Post(userId = 2, title = "Post 3", text = "Body 3")) } } } } }
view directory
File /view/MainView.kt
interface MainView: LifecycleOwner { fun render(state: PostState) }
File /view/activity/MainActivity.kt
class MainActivity: AppCompatActivity(), MainView { private lateinit var presenter: MainPresenter private lateinit var adapter: PostListAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setupPresenter() setupViews() } private fun setupPresenter() { presenter = MainPresenter(PostInteractor()) presenter.bind(this) } override fun render(state: PostState) { when (state) { is PostState.LoadingState -> renderLoadingState() is PostState.DataState -> renderDataState(state) is PostState.ErrorState -> renderErrorState(state) } } private fun renderLoadingState() { rvItems.isEnabled = false Log.d("TAG", "LOADING"); } private fun renderDataState(dataState: PostState.DataState) { rvItems.apply { isEnabled = true (adapter as PostListAdapter).setPosts(dataState.data) } } private fun renderErrorState(dataState: PostState.ErrorState) { Toast.makeText(this@MainActivity, dataState.data, Toast.LENGTH_LONG).show() } private fun setupViews() { rvItems.layoutManager = LinearLayoutManager(this) adapter = PostListAdapter(mutableListOf()) rvItems.adapter = adapter } }
File activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout 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" tools:context=".view.activity.MainActivity"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rvItems" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerInParent="true" /> </RelativeLayout>
File /view/adapter/PostListAdapter.kt
class PostListAdapter(private val posts: MutableList<Post>) : RecyclerView.Adapter<PostListAdapter.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>