Skip to content

Instantly share code, notes, and snippets.

@Pooh3Mobi
Last active May 8, 2022 23:21
Show Gist options
  • Save Pooh3Mobi/219e7ae5f2bf35edd69129250785ce65 to your computer and use it in GitHub Desktop.
Save Pooh3Mobi/219e7ae5f2bf35edd69129250785ce65 to your computer and use it in GitHub Desktop.
RecyclerView Sample in 2022 (for my memo)
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
compileSdk 32
defaultConfig {
applicationId "com.example.recyclerviewsample"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.6.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation "androidx.recyclerview:recyclerview:1.2.1"
// For control over item selection of both touch and mouse driven selection
// implementation "androidx.recyclerview:recyclerview-selection:1.1.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.ext.versions.lifecycle"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.ext.versions.lifecycle"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$rootProject.ext.versions.lifecycle"
implementation "androidx.lifecycle:lifecycle-common-java8:$rootProject.ext.versions.lifecycle"
// DI
implementation "com.google.dagger:hilt-android:$rootProject.ext.versions.hilt"
kapt "com.google.dagger:hilt-android-compiler:$rootProject.ext.versions.hilt"
// implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"
kapt 'androidx.hilt:hilt-compiler:1.0.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.1.3' apply false
id 'com.android.library' version '7.1.3' apply false
id 'org.jetbrains.kotlin.android' version '1.6.0' apply false
id 'com.google.dagger.hilt.android' version '2.41' apply false
}
ext {
versions = [
"lifecycle" : "2.4.1",
"hilt" : "2.41",
]
}
task clean(type: Delete) {
delete rootProject.buildDir
}
package com.example.recyclerviewsample.ui
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
import com.example.recyclerviewsample.R
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
@AndroidEntryPoint
class MainFragment : Fragment(R.layout.fragment_main) {
private val viewModel: MainViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val listView = view.findViewById<RecyclerView>(R.id.listView).apply {
adapter = MyAdapter()
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
listView.submitList(uiState.data)
uiState.userMessages.firstOrNull()?.let { userMessage ->
showToast(userMessage.message)
viewModel.userMessageShown(userMessage.id)
}
}
}
}
}
private fun showToast(message: String) =
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
private fun RecyclerView.submitList(itemDataList: List<ItemDataItemUiState>) =
(this.adapter as MyAdapter).submitList(itemDataList)
}
package com.example.recyclerviewsample.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.recyclerviewsample.data.ItemDataRepository
import com.example.recyclerviewsample.model.ItemData
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.util.*
import javax.inject.Inject
data class UserMessage(
val id: Long,
val message: String
)
data class ItemDataItemUiState(
val id: String,
val title: String,
val onItemLongClick: () -> Unit,
val onItemClick: () -> Unit,
)
data class MainUiState(
val data: List<ItemDataItemUiState> = emptyList(),
val userMessages: List<UserMessage> = emptyList()
) {
fun updateData(newData: List<ItemDataItemUiState>) =
this.copy(data = newData)
fun addMessage(id: Long = UUID.randomUUID().mostSignificantBits, message: String) =
this.copy(userMessages = userMessages + UserMessage(id, message))
fun removeMessage(id: Long) =
this.copy(userMessages = userMessages.filterNot { it.id == id })
}
@HiltViewModel
class MainViewModel @Inject constructor(
private val itemDataRepository: ItemDataRepository,
) : ViewModel() {
private val _uiState = MutableStateFlow(MainUiState())
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()
init {
viewModelScope.launch {
val data = itemDataRepository.getAll()
_uiState.update { currentUiState ->
currentUiState.updateData(data.map { it.toItemUiState() })
}
}
}
private fun remove(itemData: ItemData) {
viewModelScope.launch {
itemDataRepository.remove(itemData)
val data = itemDataRepository.getAll()
_uiState.update { currentUiState ->
currentUiState.updateData(data.map { it.toItemUiState() })
}
}
}
private fun showItemDataInfo(itemData: ItemData) {
_uiState.update { currentUiState ->
currentUiState.addMessage(message = "clicked item: $itemData")
}
}
fun userMessageShown(messageId: Long) {
_uiState.update { currentUiState ->
currentUiState.removeMessage(messageId)
}
}
private fun ItemData.toItemUiState(): ItemDataItemUiState {
return ItemDataItemUiState(
id = id,
title = name,
onItemLongClick = { this@MainViewModel.remove(this) },
onItemClick = { this@MainViewModel.showItemDataInfo(this) }
)
}
}
package com.example.recyclerviewsample
import com.example.recyclerviewsample.data.ItemDataRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import javax.inject.Qualifier
import javax.inject.Singleton
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class IoDispatcher
@Module
@InstallIn(SingletonComponent::class)
class RepositoryModule {
@Singleton
@Provides
fun provideItemDataRepository(
@IoDispatcher coroutineDispatcher: CoroutineDispatcher,
): ItemDataRepository = ItemDataRepository(coroutineDispatcher)
}
@Module
@InstallIn(SingletonComponent::class)
class CoroutineDispatcherModule {
@IoDispatcher
@Provides
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
}
package com.example.recyclerviewsample.ui
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.recyclerviewsample.R
private typealias ItemUiState = ItemDataItemUiState
class MyAdapter : ListAdapter<ItemDataItemUiState, RecyclerView.ViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.simple_text_item, parent, false)
return object : RecyclerView.ViewHolder(view) {}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val itemUiState = getItem(position)
val view = holder.itemView.apply {
setOnClickListener { itemUiState.onItemClick() }
setOnLongClickListener { itemUiState.onItemLongClick(); true }
}
val textView = view.findViewById<TextView>(R.id.textView)
textView.text = itemUiState.title
}
}
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ItemUiState>() {
override fun areItemsTheSame(old: ItemUiState, new: ItemUiState) =
old.id == new.id
override fun areContentsTheSame(old: ItemUiState, new: ItemUiState) =
old == new
}
@Pooh3Mobi
Copy link
Author

Pooh3Mobi commented May 7, 2022

Warning: It's bad practice to pass the ViewModel into the RecyclerView adapter because that tightly couples the adapter with the ViewModel class.

https://developer.android.com/topic/architecture/ui-layer/events#recyclerview-events

RecyclerViewのAdapterにViewModelは渡さないこと。AdapterとViewModelが密結合してしまうバッドプラクティスです。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment