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