MVVM stands for Model-View-ViewModel. MVVM 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:
The data package has three packages related to the backend of your app:
Post model and the PostResponse model.The net package contains the files required by Retrofit to communicate with the web service: PostAPI.kt and RetrofitClient.kt.
The view package contains three packages related to the front end of your app such as the Activities and Adapters.
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" def retrofitVersion = "2.4.0" implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" 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" implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
AndroidManifest.xml
<application android:name=".App" ...>
<activity android:name=".view.activities.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/PostRepository.kt
interface PostRepository {
fun list(): LiveData<List<Post>>
fun save(post: Post)
fun delete(post: Post)
}
File /data/PostRepositoryImpl.kt
class PostRepositoryImpl: PostRepository {
private val postDao: PostDao = db.postDao()
private val allPosts: LiveData<List<Post>>
init {
allPosts = postDao.getAll()
}
override fun delete(post: Post) {
thread {
db.postDao().delete(post.id)
}
}
override fun list() = allPosts
override fun save(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 {
private val postApi: PostApi
companion object {
const val BASE_URL = "https://jsonplaceholder.typicode.com/"
}
init {
val builder = OkHttpClient.Builder()
val okHttpClient = builder.build()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
postApi = retrofit.create(PostApi::class.java)
}
}
view directory
File /view/activities/MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private val adapter = PostListAdapter(mutableListOf())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
rvItems.adapter = adapter
showLoading()
viewModel.getSavedPosts().observe(this, Observer { posts ->
posts?.let {
if (posts.isEmpty()) {
viewModel.add(Post(userId = 1, title = "Post 1", text = "Body 1"))
viewModel.add(Post(userId = 1, title = "Post 2", text = "Body 2"))
}
hideLoading()
adapter.setPosts(posts)
Log.d("TAG", "$posts");
}
})
}
private fun showLoading() {
rvItems.isEnabled = false
progressBar.visibility = View.VISIBLE
}
private fun hideLoading() {
rvItems.isEnabled = true
progressBar.visibility = View.GONE
}
}
File activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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=".view.activities.MainActivity">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvItems"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
File /view/adapters/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>
viewmodel directory
File /viewmodel/MainViewModel.kt
class MainViewModel(private val repository: PostRepository = PostRepositoryImpl()) : ViewModel() {
private val allPosts = MediatorLiveData<List<Post>>()
init {
getAllPosts()
}
fun getSavedPosts() = allPosts
private fun getAllPosts() {
allPosts.addSource(repository.list()) { posts ->
allPosts.postValue(posts)
}
}
fun delete(post: Post) {
repository.delete(post)
}
fun add(post: Post) {
repository.save(post)
}
}