I've created a simple project to study Kotlin and Android architecture
https://github.com/AOreshin/shtatus
The screen consists of RecyclerView and three EditTexts.
Corresponding ViewModel is exposing 7 LiveData's:
Three LiveData corresponding to filters
Event to notify the user that no entries are found
Event to notify the user that no entries are present
Status of SwipeRefreshLayout
List of connections to show based on filter input
When user types text in filter ViewModel's LiveData gets notified about the changes and updates the data. I 've read that it's a bad practice to expose MutableLiveData to Activities/Fragments but they have to notify ViewModel about the changes somehow. When no entries are found based on the user's input Toast is shown.
The problem
When the user enters filter values that have no matches, Toast is shown. If the user then rotates the device Toast is shown again and again.
I've read these articles:
https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
https://proandroiddev.com/livedata-with-single-events-2395dea972a8
But I don't understand how I can apply these to my use case. I think the problem in how I perform the updates
private val connections = connectionRepository.allConnections()
private val mediatorConnection = MediatorLiveData<List<Connection>>().also {
it.value = connections.value
}
private val refreshLiveData = MutableLiveData(RefreshStatus.READY)
private val noMatchesEvent = SingleLiveEvent<Void>()
private val emptyTableEvent = SingleLiveEvent<Void>()
val nameLiveData = MutableLiveData<String>()
val urlLiveData = MutableLiveData<String>()
val actualStatusLiveData = MutableLiveData<String>()
init {
with(mediatorConnection) {
addSource(connections) { update() }
addSource(nameLiveData) { update() }
addSource(urlLiveData) { update() }
addSource(actualStatusLiveData) { update() }
}
}
fun getRefreshLiveData(): LiveData<RefreshStatus> = refreshLiveData
fun getNoMatchesEvent(): LiveData<Void> = noMatchesEvent
fun getEmptyTableEvent(): LiveData<Void> = emptyTableEvent
fun getConnections(): LiveData<List<Connection>> = mediatorConnection
private fun update() {
if (connections.value.isNullOrEmpty()) {
emptyTableEvent.call()
} else {
mediatorConnection.value = connections.value?.filter { connection -> getPredicate().test(connection) }
if (mediatorConnection.value.isNullOrEmpty()) {
noMatchesEvent.call()
}
}
}
update() gets triggered on screen rotation because of new subscription to mediatorConnection and MediatorLiveData.onActive() is called. And it's intented behavior
Android live data - observe always fires after config change
Code for showing toast
package com.github.aoreshin.shtatus.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.Toast
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.github.aoreshin.shtatus.R
import com.github.aoreshin.shtatus.ShatusApplication
import com.github.aoreshin.shtatus.viewmodels.ConnectionListViewModel
import javax.inject.Inject
class ConnectionListFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var refreshLayout: SwipeRefreshLayout
private lateinit var nameEt: EditText
private lateinit var urlEt: EditText
private lateinit var statusCodeEt: EditText
private lateinit var viewModel: ConnectionListViewModel
private lateinit var recyclerView: RecyclerView
private lateinit var viewAdapter: ConnectionListAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_connection_list, container, false)
val application = (requireActivity().application as ShatusApplication)
application.appComponent.inject(this)
val viewModelProvider = ViewModelProvider(this, viewModelFactory)
viewModel = viewModelProvider.get(ConnectionListViewModel::class.java)
bindViews(view)
setupObservers()
setupListeners()
addFilterValues()
setupRecyclerView()
return view
}
private fun setupObservers() {
with(viewModel) {
getConnections().observe(viewLifecycleOwner, Observer { viewAdapter.submitList(it) })
getRefreshLiveData().observe(viewLifecycleOwner, Observer { status ->
when (status) {
ConnectionListViewModel.RefreshStatus.LOADING -> refreshLayout.isRefreshing = true
ConnectionListViewModel.RefreshStatus.READY -> refreshLayout.isRefreshing = false
else -> throwException(status.toString())
}
})
getNoMatchesEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_matches) })
getEmptyTableEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_connections) })
}
}
private fun setupRecyclerView() {
viewAdapter = ConnectionListAdapter(parentFragmentManager, ConnectionItemCallback())
recyclerView.apply {
layoutManager = LinearLayoutManager(context)
adapter = viewAdapter
}
}
private fun addFilterValues() {
with(viewModel) {
nameEt.setText(nameLiveData.value)
urlEt.setText(urlLiveData.value)
statusCodeEt.setText(actualStatusLiveData.value)
}
}
private fun bindViews(view: View) {
with(view) {
recyclerView = findViewById(R.id.recycler_view)
refreshLayout = findViewById(R.id.refresher)
nameEt = findViewById(R.id.nameEt)
urlEt = findViewById(R.id.urlEt)
statusCodeEt = findViewById(R.id.statusCodeEt)
}
}
private fun setupListeners() {
with(viewModel) {
refreshLayout.setOnRefreshListener { send() }
nameEt.addTextChangedListener { nameLiveData.value = it.toString() }
urlEt.addTextChangedListener { urlLiveData.value = it.toString() }
statusCodeEt.addTextChangedListener { actualStatusLiveData.value = it.toString() }
}
}
private fun throwException(status: String) {
throw IllegalStateException(getString(R.string.error_no_such_status) + status)
}
private fun showToast(resourceId: Int) {
Toast.makeText(context, getString(resourceId), Toast.LENGTH_SHORT).show()
}
override fun onDestroyView() {
super.onDestroyView()
with(viewModel) {
getNoMatchesEvent().removeObservers(viewLifecycleOwner)
getRefreshLiveData().removeObservers(viewLifecycleOwner)
getEmptyTableEvent().removeObservers(viewLifecycleOwner)
getConnections().removeObservers(viewLifecycleOwner)
}
}
}
How I should address this issue?
After some head scratching I've decided to go with internal ViewModel statuses, this way logic in Activity/Fragment is kept to a minimum.
So now my ViewModel looks like this:
package com.github.aoreshin.shtatus.viewmodels
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.github.aoreshin.shtatus.events.SingleLiveEvent
import com.github.aoreshin.shtatus.room.Connection
import io.reactivex.FlowableSubscriber
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import io.reactivex.subscribers.DisposableSubscriber
import okhttp3.ResponseBody
import retrofit2.Response
import java.util.function.Predicate
import javax.inject.Inject
import javax.inject.Singleton
#Singleton
class ConnectionListViewModel #Inject constructor(
private val connectionRepository: ConnectionRepository
) : ViewModel() {
private var tableStatus = TableStatus.OK
private val connections = connectionRepository.allConnections()
private val mediatorConnection = MediatorLiveData<List<Connection>>()
private val stopRefreshingEvent = SingleLiveEvent<Void>()
private val noMatchesEvent = SingleLiveEvent<Void>()
private val emptyTableEvent = SingleLiveEvent<Void>()
private val nameLiveData = MutableLiveData<String>()
private val urlLiveData = MutableLiveData<String>()
private val statusLiveData = MutableLiveData<String>()
init {
with(mediatorConnection) {
addSource(connections) { update() }
addSource(nameLiveData) { update() }
addSource(urlLiveData) { update() }
addSource(statusLiveData) { update() }
}
}
fun getStopRefreshingEvent(): LiveData<Void> = stopRefreshingEvent
fun getNoMatchesEvent(): LiveData<Void> = noMatchesEvent
fun getEmptyTableEvent(): LiveData<Void> = emptyTableEvent
fun getConnections(): LiveData<List<Connection>> = mediatorConnection
fun getName(): String? = nameLiveData.value
fun getUrl(): String? = urlLiveData.value
fun getStatus(): String? = statusLiveData.value
fun setName(name: String) { nameLiveData.value = name }
fun setUrl(url: String) { urlLiveData.value = url }
fun setStatus(status: String) { statusLiveData.value = status }
private fun update() {
if (connections.value != null) {
if (connections.value.isNullOrEmpty()) {
if (tableStatus != TableStatus.EMPTY) {
emptyTableEvent.call()
tableStatus = TableStatus.EMPTY
}
} else {
mediatorConnection.value = connections.value?.filter { connection -> getPredicate().test(connection) }
if (mediatorConnection.value.isNullOrEmpty()) {
if (tableStatus != TableStatus.NO_MATCHES) {
noMatchesEvent.call()
tableStatus = TableStatus.NO_MATCHES
}
} else {
tableStatus = TableStatus.OK
}
}
}
}
fun send() {
if (!connections.value.isNullOrEmpty()) {
val singles = connections.value?.map { connection ->
val id = connection.id
val description = connection.description
val url = connection.url
var message = ""
connectionRepository.sendRequest(url)
.doOnSuccess { message = it.code().toString() }
.doOnError { message = it.message!! }
.doFinally {
val result = Connection(id, description, url, message)
connectionRepository.insert(result)
}
}
Single.mergeDelayError(singles)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally { stopRefreshingEvent.call() }
.subscribe(getSubscriber())
} else {
stopRefreshingEvent.call()
}
}
private fun getSubscriber() : FlowableSubscriber<Response<ResponseBody>> {
return object: DisposableSubscriber<Response<ResponseBody>>() {
override fun onComplete() { Log.d(TAG, "All requests sent") }
override fun onNext(t: Response<ResponseBody>?) { Log.d(TAG, "Request is done") }
override fun onError(t: Throwable?) { Log.d(TAG, t!!.message!!) }
}
}
private fun getPredicate(): Predicate<Connection> {
return Predicate { connection ->
connection.description.contains(nameLiveData.value.toString(), ignoreCase = true)
&& connection.url.contains(urlLiveData.value.toString(), ignoreCase = true)
&& connection.actualStatusCode.contains(
statusLiveData.value.toString(),
ignoreCase = true
)
}
}
private enum class TableStatus {
NO_MATCHES,
EMPTY,
OK
}
companion object {
private const val TAG = "ConnectionListViewModel"
}
}
And corresponding Fragment looks like this:
package com.github.aoreshin.shtatus.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.Toast
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.github.aoreshin.shtatus.R
import com.github.aoreshin.shtatus.ShatusApplication
import com.github.aoreshin.shtatus.viewmodels.ConnectionListViewModel
import javax.inject.Inject
class ConnectionListFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var refreshLayout: SwipeRefreshLayout
private lateinit var nameEt: EditText
private lateinit var urlEt: EditText
private lateinit var statusCodeEt: EditText
private lateinit var viewModel: ConnectionListViewModel
private lateinit var recyclerView: RecyclerView
private lateinit var viewAdapter: ConnectionListAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_connection_list, container, false)
val application = (requireActivity().application as ShatusApplication)
application.appComponent.inject(this)
val viewModelProvider = ViewModelProvider(this, viewModelFactory)
viewModel = viewModelProvider.get(ConnectionListViewModel::class.java)
bindViews(view)
setupObservers()
setupListeners()
addFilterValues()
setupRecyclerView()
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (savedInstanceState != null) {
refreshLayout.isRefreshing = savedInstanceState.getBoolean(REFRESHING, false)
}
}
private fun setupObservers() {
with(viewModel) {
getConnections().observe(viewLifecycleOwner, Observer { viewAdapter.submitList(it) })
getStopRefreshingEvent().observe(viewLifecycleOwner, Observer { refreshLayout.isRefreshing = false })
getNoMatchesEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_matches) })
getEmptyTableEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_connections) })
}
}
private fun setupRecyclerView() {
viewAdapter = ConnectionListAdapter(parentFragmentManager, ConnectionItemCallback())
recyclerView.apply {
layoutManager = LinearLayoutManager(context)
adapter = viewAdapter
}
}
private fun addFilterValues() {
with(viewModel) {
nameEt.setText(getName())
urlEt.setText(getUrl())
statusCodeEt.setText(getStatus())
}
}
private fun bindViews(view: View) {
with(view) {
recyclerView = findViewById(R.id.recycler_view)
refreshLayout = findViewById(R.id.refresher)
nameEt = findViewById(R.id.nameEt)
urlEt = findViewById(R.id.urlEt)
statusCodeEt = findViewById(R.id.statusCodeEt)
}
}
private fun setupListeners() {
with(viewModel) {
refreshLayout.setOnRefreshListener { send() }
nameEt.addTextChangedListener { setName(it.toString()) }
urlEt.addTextChangedListener { setUrl(it.toString()) }
statusCodeEt.addTextChangedListener { setStatus(it.toString()) }
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(REFRESHING, refreshLayout.isRefreshing)
}
private fun showToast(resourceId: Int) {
Toast.makeText(context, getString(resourceId), Toast.LENGTH_SHORT).show()
}
override fun onDestroyView() {
super.onDestroyView()
with(viewModel) {
getNoMatchesEvent().removeObservers(viewLifecycleOwner)
getEmptyTableEvent().removeObservers(viewLifecycleOwner)
getStopRefreshingEvent().removeObservers(viewLifecycleOwner)
getConnections().removeObservers(viewLifecycleOwner)
}
}
companion object {
private const val REFRESHING = "isRefreshing"
}
}
Pros
No additional dependencies
Usage of widespread SingleLiveEvent
Pretty straightforward to implement
Cons
Conditional logic is quickly getting out of hand even in this simple case, surely needs refactoring. Not sure if this approach will work in real-life complex scenarios.
If there are cleaner and more concise approaches to solve this problem I will be happy to hear about them!
In your solution; you introduced TableStatus which is acting like flag and not needed.
If you really looking for a good Android Architecture; instead you could just do
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
showToast(R.string.status_no_connections)
and
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
showToast(R.string.status_no_matches)
NOTE:
viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED
is not a patch Google implemented this fix in support library as well.
And remove #Singleton from (why would you need it to be singleton)
#Singleton
class ConnectionListViewModel #Inject constructor(
PS:
From the top of my head looks like; you may also don't need SingleLiveEvent for you case.
(would love to talk more on this if you want I also just have started Kotlin + Clear & Scalable Android Architecture)
Related
I am simply trying to setup the ClickListener for changing the user name. Google's tutorials emphasize fragments, which I will do later. Based on what I've seen in other examples and the Android documentation, I thought I had the View Binding set up properly. I don't have enough information to understand why I see a list of copies of the activity screen inside the RecyclerView.
UserListAdapter.kt
package com.neillbarrett.debitsandcredits
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.neillbarrett.debitsandcredits.database.UsersTable
import com.neillbarrett.debitsandcredits.databinding.ActivityManageUsersBinding
val inAdapter: String = "In UserListAdapter "
class UserListAdapter(private val userSelect: (UsersTable?) -> Unit) :
ListAdapter<UsersTable, UserListAdapter.UserViewHolder>(UsersComparator()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserListAdapter.UserViewHolder {
Log.w(inAdapter,"OnCreateViewHolder started")
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.activity_manage_users, parent, false)
return UserViewHolder.create(parent)
}
class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(usersTable: UsersTable?, userSelect: (UsersTable?) -> Unit) {
Log.w(inAdapter,"UserViewHolder started")
itemView.setOnClickListener { View.OnClickListener {
/* if (View.) { }*/
val nameSelected = userSelect(usersTable)
//userSelect(usersTable)
//need to assign the result of the clicklistener to the editText
//binding.etEditName.setText(R.layout.activity_list_of_users.toString())
}}
}
companion object {
fun create(parent: ViewGroup) : UserViewHolder {
Log.w(inAdapter,"Companion object 'Create' function started")
val view: View = LayoutInflater.from(parent.context)
.inflate(R.layout.activity_manage_users, parent, false)
return UserViewHolder(view)
}
}
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
Log.w(inAdapter,"OnBindViewHolder started")
val current = getItem(position)
holder.bind(current, userSelect)
}
class UsersComparator : DiffUtil.ItemCallback<UsersTable>() {
override fun areItemsTheSame(oldItem: UsersTable, newItem: UsersTable): Boolean {
Log.w(inAdapter,"areItemsTheSame function started")
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: UsersTable, newItem: UsersTable): Boolean {
Log.w(inAdapter,"areContentsTheSame function started")
return oldItem.userName == newItem.userName
}
}
}
ManageUsers.kt
package com.neillbarrett.debitsandcredits
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.activity.viewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.neillbarrett.debitsandcredits.database.CreditsAndDebitsApp
import com.neillbarrett.debitsandcredits.database.UsersTable
import com.neillbarrett.debitsandcredits.databinding.ActivityManageUsersBinding
class ManageUsers : AppCompatActivity() {
lateinit var binding: ActivityManageUsersBinding
lateinit var recyclerView: RecyclerView
lateinit var editTextAddUser: EditText
lateinit var editTextChangeUser: EditText
lateinit var newUser: String
var userSelect: ((UsersTable?) -> Unit) = {}
var position: Long = 0
val inWhichActivity: String = "In ManageUsers"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityManageUsersBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
//setContentView(R.layout.activity_manage_users)
Log.w(inWhichActivity, "Setting up userViewModel & repository")
val userViewModel: UserViewModel by viewModels {
UserViewModelFactory((application as CreditsAndDebitsApp).repository)
}
recyclerView = findViewById(R.id.rec_view_userList)
editTextAddUser = findViewById(R.id.et_AddUser)
editTextChangeUser = findViewById(R.id.et_Edit_Name)
val adapter = UserListAdapter(userSelect)
binding.recViewUserList.adapter = adapter
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
userViewModel.allUsers.observe(this, Observer() {user ->
Log.w(inWhichActivity,"Starting Observer")
user?.let { adapter.submitList(it) }
Log.w(inWhichActivity, "Started Observer")
})
val btnAddUser = findViewById<Button>(R.id.btn_AddUser)
btnAddUser.setOnClickListener {
Log.w(inWhichActivity,"Started btnAddUser.setOnClickListener")
if (TextUtils.isEmpty(editTextAddUser.text)) {
Toast.makeText(this, "User name cannot be empty", Toast.LENGTH_SHORT).show()
} else {
newUser = editTextAddUser.text.toString()
// Log.w("Add user button", "Username put into newUser")
userViewModel.insertUser(UsersTable(0, newUser))
// Toast.makeText(this, "Username added to table", Toast.LENGTH_SHORT).show()
// Log.w("Add user button", "Username added to table")
}
}
val btnChangeUser = findViewById<Button>(R.id.btn_ChangeUserName)
btnChangeUser.setOnClickListener {
Log.w(inWhichActivity,"Started btnChangeUser.setOnClickListener")
Toast.makeText(this, "Selected position is ${recyclerView.getChildAdapterPosition(it)}", Toast.LENGTH_SHORT).show()
/* if (recyclerView.getChildAdapterPosition(it) == -1) {
Toast.makeText(this, "Select a name.", Toast.LENGTH_SHORT).show()
} else {
if (editTextChangeUser.text.toString() == recyclerView.adapter.toString()) {
Toast.makeText(this, "Name has not been changed.", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Name would have been changed.", Toast.LENGTH_SHORT).show()
val rvItemRecId: Long
rvItemRecId = adapter.getItemId(position.toInt())
userViewModel.updateUser(UsersTable(rvItemRecId.toInt(), adapter.toString()))
}
}*/
}
}
}
UserViewModel.kt
package com.neillbarrett.debitsandcredits
import android.util.Log
import androidx.lifecycle.*
import com.neillbarrett.debitsandcredits.database.UsersTable
import kotlinx.coroutines.launch
import java.lang.IllegalArgumentException
val inViewModel: String = "In UserViewModel "
class UserViewModel(private val repository: UserRepository) : ViewModel() {
// Using LiveData and caching what allWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allUsers: LiveData<List<UsersTable>> = repository.allUsers.asLiveData()
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insertUser(user: UsersTable) = viewModelScope.launch {
repository.insertUser(user)
Log.w(inViewModel,"insertUser called")
//repository.insertUser(usersTable = List<UsersTable>())
//repository.insertUser(UsersTable(0, userName = user.userName))
}
fun updateUser(user: UsersTable) = viewModelScope.launch {
repository.updateUser(user)
Log.w(inViewModel, "updateUser called")
}
fun deleteUser(user: UsersTable) = viewModelScope.launch {
repository.deleteUser(user)
Log.w(inViewModel, "deteUser called")
}
}
class UserViewModelFactory(private val repository: UserRepository) : ViewModelProvider.Factory{
override fun <T : ViewModel> create(modelClass: Class<T>): T {
Log.w(inViewModel,"UserViewModelFactory called")
if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return UserViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
I have been trying to call a function from fragment to adapter, but I can't approach it right.
I want to invisible the button present in the fragment from adapter.
**My Adapter Code:**
package com.littleboo.brandlogo.Adapters
import android.R
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat.getSystemService
import androidx.recyclerview.widget.RecyclerView
import com.littleboo.brandlogo.Fragments.QuizFragment
import com.littleboo.brandlogo.MainActivity
import com.littleboo.brandlogo.Models.Question
import com.littleboo.brandlogo.databinding.ActivityMainBinding.inflate
import com.littleboo.brandlogo.databinding.FragmentQuizBinding
import kotlinx.coroutines.NonDisposableHandle.parent
class QuestionAdap(val context: Context, val question: Question) :
RecyclerView.Adapter<QuestionAdap.OptionViewHolder>() {
var index = 1
var score = 0
val animShake: Animation = AnimationUtils.loadAnimation(context, com.littleboo.brandlogo.R.anim.shake)
private var options: List<String> = listOf(question.option1, question.option2, question.option3, question.option4)
inner class OptionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
var optionView = itemView.findViewById<TextView>(com.littleboo.brandlogo.R.id.quiz_option)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionViewHolder {
val view = LayoutInflater.from(context).inflate(com.littleboo.brandlogo.R.layout.quizoptions, parent, false)
return OptionViewHolder(view)
}
override fun onBindViewHolder(holder: OptionViewHolder, position: Int) {
holder.optionView.text = options[position]
holder.itemView.setOnClickListener {
question.userAnswer = options[position]
notifyDataSetChanged()
}
if(question.userAnswer == options[position] && question.userAnswer == question.answer){
holder.itemView.setBackgroundResource(com.littleboo.brandlogo.R.drawable.option_item_selected_bg)
score += 10
Toast.makeText(context,"Score is $score", Toast.LENGTH_SHORT).show()
}
else if(question.userAnswer == options[position] && question.userAnswer != question.answer){
holder.itemView.setBackgroundResource(com.littleboo.brandlogo.R.drawable.wrong_option_item_selected_bg)
holder.itemView.startAnimation(animShake)
}
else{
holder.itemView.setBackgroundResource(com.littleboo.brandlogo.R.drawable.non_option_item_selected_bg)
}
}
override fun getItemCount(): Int {
return options.size
}
}
My Quiz Fragment Code:
package com.littleboo.brandlogo.Fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.bumptech.glide.Glide
import com.google.firebase.firestore.FirebaseFirestore
import com.littleboo.brandlogo.Adapters.QuestionAdap
import com.littleboo.brandlogo.Models.Question
import com.littleboo.brandlogo.Models.quizmodel
import com.littleboo.brandlogo.R
import com.littleboo.brandlogo.databinding.FragmentQuizBinding
class QuizFragment : Fragment(){
lateinit var binding: FragmentQuizBinding
var quizzes: MutableList<quizmodel>? = null
private var questions = mutableMapOf<String, Question>()
private lateinit var mArraylist: ArrayList<Question>
var index = 1
private lateinit var mfirestore: FirebaseFirestore
private lateinit var mrecycler: RecyclerView
lateinit var myadapter: QuestionAdap
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentQuizBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mrecycler = binding.optionList
mrecycler.layoutManager
myadapter = QuestionAdap(requireContext(),Question())
mArraylist = arrayListOf()
questions.map { it.key to it.value }.shuffled().toMap()
setUpFirestore()
setUpEventListener()
}
override fun onDestroy() {
super.onDestroy()
mfirestore.terminate()
// finish()
}
private fun setUpEventListener() {
binding.nextbtn.setOnClickListener {
index++
bindViews()
}
// binding.btnSubmit.setOnClickListener {
// Log.d("FINALQUIZ", questions.toString())
// val intent = Intent(this, ResultActivity::class.java)
// val json = Gson().toJson(quizzes!![0])
// intent.putExtra("QUIZ", json)
// startActivity(intent)
// finish()
// }
}
private fun setUpFirestore() {
mfirestore = FirebaseFirestore.getInstance()
val quizTitle = activity?.intent?.getStringExtra("title")
if (quizTitle != null) {
mfirestore.collection("Quizes").whereEqualTo("title", quizTitle)
.get()
.addOnSuccessListener {
if (it != null && !it.isEmpty) {
quizzes = it.toObjects(quizmodel::class.java)
questions = quizzes!![0].questions
shuffle()
bindViews()
}
}
}
}
private fun bindViews() {
// btnPrevious.visibility = View.GONE
// binding.btnSubmit.visibility = View.GONE
// binding.btnNext.visibility = View.GONE
// if (index == 1) { //first question
// binding.btnNext.visibility = View.VISIBLE
// } else if (index == questions!!.size) { // last question
// binding.btnSubmit.visibility = View.VISIBLE
//// btnPrevious.visibility = View.VISIBLE
// } else { // Middle
//// btnPrevious.visibility = View.VISIBLE
// binding.btnNext.visibility = View.VISIBLE
// }
val circularProgressDrawable = CircularProgressDrawable(requireContext())
circularProgressDrawable.strokeWidth = 8f
// circularProgressDrawable.colorFilter = ("#ac5fe1")
circularProgressDrawable.centerRadius = 30f
circularProgressDrawable.start()
val question = questions!!["question$index"]
question?.let {
Glide.with(this).load(it.imagequiz).placeholder(circularProgressDrawable).into(binding.imagequiz)
val optionAdapter = QuestionAdap(requireContext(), it)
mrecycler.layoutManager = LinearLayoutManager(requireContext())
mrecycler.adapter = optionAdapter
mrecycler.setHasFixedSize(true)
}
}
private fun shuffle() {
val keys = questions.keys.toMutableList().shuffled()
val values = questions.values.toMutableList().shuffled()
keys.forEachIndexed { index, key ->
questions[key] = values[index]
}
}
}
I tried calling fragment in adapter like:
QuizFragment().binding.btnxt.visibility = View.Visible
in BindViewHolder function.
Thank You
You can use kotlin lambda function to achieve it. Like this
class QuestionAdap(val context: Context, val question: Question, var onItemClicked: ((boolean) -> Unit))
call onItemClicked in your adapter where you want it
and in fragment
myadapter = QuestionAdap(requireContext(),Question()) { boolean ->
if (boolean) {
//hide/show
} else {
//hide/show
}
}
You can use the Callback functions of kotlin in your fragment, which will be passed into your adapter. So whenever you invoke that callback from your adapter, it will be triggered in your fragment.
Step 1: Create a method like the one below in your fragment.
private fun showHideButtonFromAdapter (
isButtonVisible: Boolean
) {
// set your button visibility according to isButtonVisible value.
}
Step 2: pass a method from your fragment to adapter as argument
val adapter = YourAdapter(::showHideButtonFromAdapter)
// set above adapter in your recycler view.
Step 3: In your adapter invoke that callback function like the one below.
class ColorPickerAdapter constructor(
private val onItemClicked: (Boolean) -> Unit
) : RecyclerView.Adapter<YourAdapter.ViewHolder>() {
// your other adapter methods here
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
onItemClicked.invoke(pass true or false as per your requirement)
// above invocation will trigger an event in the fragment.
}
}
In your adapter there need to be instance of your fragment, so change it like:
class QuestionAdap(val context: Context, val question: Question, fragment: Fragment) :
RecyclerView.Adapter<QuestionAdap.OptionViewHolder>() {
Then you can call your fragment by simply writing fragment in your adapter like:
override fun onBindViewHolder(holder: OptionViewHolder, position: Int) {
holder.optionView.text = options[position]
holder.itemView.setOnClickListener {
question.userAnswer = options[position]
notifyDataSetChanged()
}
//Example
fragment.shuffle()
And you need to send this fragment instance when you create it's adapter, so in your fragment use this:
myadapter = QuestionAdap(requireContext(),Question(), this)
I'm currently writing an app that displays a list of movies. I have many fragments that display a cardview containing movies, and each cardview has a checkbox. The user can press on the cardview to go to the details page of the movie where another checkbox is present.
The goal of both checkboxes is to add the movie to the favorites tab.
My question is, how can I make the checkbox that is inside the details page checked when the user checks the one in the cardview?
Below is the relevant code.
Appreciate all the help I can get.
MoviesListFragment.kt
package com.example.moviesapp.ui.Fragments
import android.os.Bundle
import android.view.*
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.moviesapp.R
import com.example.moviesapp.databinding.FragmentMoviesListBinding
import com.example.moviesapp.network.MoviesFavorites
import com.example.moviesapp.network.MoviesResults
import com.example.moviesapp.ui.DaoViewModel
import com.example.moviesapp.ui.MovieApiStatus
import com.example.moviesapp.ui.MoviesListAdapter
import com.example.moviesapp.ui.MoviesListViewModel
import dagger.hilt.android.AndroidEntryPoint
#AndroidEntryPoint
class MoviesListFragment : Fragment(R.layout.fragment_movies_list), MoviesListAdapter.OnItemClickListener {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_movies_list, container, false)
}
private val daoViewModel by viewModels<DaoViewModel>()
private val viewModel by viewModels<MoviesListViewModel>()
private var _binding: FragmentMoviesListBinding? = null
private val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//View is inflated layout
_binding = FragmentMoviesListBinding.bind(view)
val adapter = MoviesListAdapter(this)
binding.apply {
recyclerView.layoutManager = LinearLayoutManager(requireContext())
//Disable animations
recyclerView.setHasFixedSize(true)
recyclerView.adapter = adapter
}
//Observe the movies livedata
//Use viewLifecycleOwner instead of this because the UI should stop being updated when the fragment view is destroyed
viewModel.getTrending()
viewModel.moviesTrending.observe(viewLifecycleOwner) {
adapter.submitList(it)
}
viewModel.networkState.observe(viewLifecycleOwner, {
binding.progressBar.isVisible = if (it==MovieApiStatus.LOADING) true else view.isGone
binding.buttonRetry.isVisible = if(it==MovieApiStatus.ERROR) true else view.isGone
binding.errorTextView.isVisible = if(it==MovieApiStatus.ERROR) true else view.isGone
binding.recyclerView.isVisible = if(it==MovieApiStatus.DONE) true else view.isGone
binding.noResultsText.isVisible = false
})
//Display trending movies
//loadstate is of type combined loadstates, which combines the loadstate of different scenarios(when we refresh dataset or when we append new data to it) into this one object
//We can use it to check for these scenarios and make our views visible or unvisible according to it
setHasOptionsMenu(true)
}
override fun onItemClick(movie: MoviesResults.Movies) {
val action = MoviesListFragmentDirections.actionMoviesListFragmentToMoviesDetailsFragment(movie)
findNavController().navigate(action)
}
override fun onFavoriteClick(favorites: MoviesFavorites) {
daoViewModel.addMovieToFavs(favorites)
}
override fun onDeleteClick(favorites: MoviesFavorites) {
daoViewModel.deleteMovieFromFavs(favorites)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
// Inflate the gallery menu
inflater.inflate(R.menu.menu_gallery, menu)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
MoviesListAdapter.kt
package com.example.moviesapp.ui
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.moviesapp.R
import com.example.moviesapp.databinding.MovieLayoutBinding
import com.example.moviesapp.network.MoviesFavorites
import com.example.moviesapp.network.MoviesResults
val IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w500"
class MoviesListAdapter constructor(private val listener: OnItemClickListener) :
ListAdapter<MoviesResults.Movies, MoviesListAdapter.MoviesListViewHolder>(DiffCallback) {
private lateinit var fav: MoviesFavorites
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MoviesListViewHolder {
val binding = MovieLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MoviesListViewHolder(binding)
}
override fun onBindViewHolder(holder: MoviesListViewHolder, position: Int) {
val currentItem = getItem(position)
holder.binding.favoritesCheckbox.isChecked = currentItem.isFavorite
holder.binding.favoritesCheckbox.setOnCheckedChangeListener { _, isChecked ->
currentItem.isFavorite
}
if(holder.binding.favoritesCheckbox.isChecked ) {
currentItem.isFavorite = true
}
if (currentItem != null) {
holder.bind(currentItem)
}
}
inner class MoviesListViewHolder(val binding: MovieLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
binding.root.setOnClickListener {
val position = absoluteAdapterPosition
if (position != RecyclerView.NO_POSITION) {
val item = getItem(position)
listener.onItemClick(item)
}
}
}
init {
binding.favoritesCheckbox.setOnClickListener{
if(binding.favoritesCheckbox.isChecked) {
val position = absoluteAdapterPosition
if (position != RecyclerView.NO_POSITION) {
val item = getItem(position)
item.isFavorite = true
fav = MoviesFavorites(item.title, item.id, item.release_date, item.overview, item.vote_average, item.poster_path, item.original_language, item.isFavorite)
listener.onFavoriteClick(fav)
listener.onCheckboxClick(binding.favoritesCheckbox.isChecked)
}
showToast("${fav.title} is added to your favorites")
}
else {
val position = absoluteAdapterPosition
if (position != RecyclerView.NO_POSITION) {
val item = getItem(position)
item.isFavorite = false
fav = MoviesFavorites(item.title, item.id, item.release_date, item.overview, item.vote_average, item.poster_path, item.original_language, item.isFavorite)
listener.onDeleteClick(fav)
listener.onCheckboxClick(binding.favoritesCheckbox.isChecked)
}
showToast("${fav.title} is removed from your favorites")
}
}
}
fun bind(movie: MoviesResults.Movies) {
binding.apply {
movieTitle.text = movie.title
movieRating.text = movie.vote_average
movieYear.text = movie.release_date
Glide.with(itemView)
.load(IMAGE_BASE_URL + movie.poster_path)
.centerCrop()
.error(R.drawable.ic_baseline_error_outline_24)
.into(movieImage)
val item = getItem(absoluteAdapterPosition)
favoritesCheckbox.isChecked = item.isFavorite
}
}
private fun showToast(string: String) {
Toast.makeText(itemView.context, string, Toast.LENGTH_SHORT).show()
}
}
interface OnItemClickListener {
fun onItemClick(movie: MoviesResults.Movies)
fun onFavoriteClick(favorites: MoviesFavorites)
fun onDeleteClick(favorites: MoviesFavorites)
fun onCheckboxClick(fav: Boolean)
}
companion object DiffCallback : DiffUtil.ItemCallback<MoviesResults.Movies>() {
override fun areItemsTheSame(
oldItem: MoviesResults.Movies,
newItem: MoviesResults.Movies
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(
oldItem: MoviesResults.Movies,
newItem: MoviesResults.Movies
): Boolean {
return oldItem == newItem
}
}
}
MoviesDetailsFragment.kt
package com.example.moviesapp.ui.Fragments
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.navArgs
import com.bumptech.glide.Glide
import com.example.moviesapp.R
import com.example.moviesapp.databinding.FragmentMoviesDetailsBinding
import com.example.moviesapp.network.MoviesFavorites
import com.example.moviesapp.network.MoviesResults
import com.example.moviesapp.ui.DaoViewModel
import com.example.moviesapp.ui.IMAGE_BASE_URL
import com.example.moviesapp.ui.SharedViewModel
import dagger.hilt.android.AndroidEntryPoint
#AndroidEntryPoint
class MoviesDetailsFragment() : Fragment(R.layout.fragment_movies_details) {
//We can get the movies from the args property
private val args by navArgs<MoviesDetailsFragmentArgs>()
private val daoViewModel by viewModels<DaoViewModel>()
private val sharedViewModel by viewModels<SharedViewModel>()
private fun showToast(string: String) {
Toast.makeText(view?.context, string, Toast.LENGTH_SHORT).show()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentMoviesDetailsBinding.bind(view)
sharedViewModel.checkBox.observe(viewLifecycleOwner) {
binding.favCheckbox.isChecked = it
}
binding.apply {
val movie: MoviesResults.Movies = args.movie
val fav = MoviesFavorites(
movie.title,
movie.id,
movie.release_date,
movie.overview,
movie.vote_average,
movie.poster_path,
movie.original_language,
movie.isFavorite,
)
//When you are in fragment/activity, pass it to a glide.with because view is less efficient
Glide.with(this#MoviesDetailsFragment)
.load(IMAGE_BASE_URL + movie.poster_path)
//Have the textview visible only when image is visible
.error(R.drawable.ic_baseline_error_outline_24)
.fitCenter()
.into(coverPhoto)
title.text = movie.title
releaseDate.text = movie.release_date
language.text = movie.original_language
rating.text = movie.vote_average
plot.text = movie.overview
favCheckbox.setOnClickListener {
if (favCheckbox.isChecked) {
fav.isFavorite = true
daoViewModel.addMovieToFavs(fav)
showToast("${fav.title} is added to your favorites")
} else {
fav.isFavorite = false
daoViewModel.deleteMovieFromFavs(fav)
showToast("${fav.title} is removed from your favorites")
}
}
}
}
}
SharedViewModel.kt
package com.example.moviesapp.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
class SharedViewModel: ViewModel() {
val checkBox = MutableLiveData<Boolean>()
fun sendValue(favorite: Boolean) {
checkBox.value = favorite
}
class SharedViewModelFactor(
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SharedViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return SharedViewModel() as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
}
You can user 2 methods to do so:
1 ) You can use LocalBroadcast to notify one fragment/activity of change in another.
Note: LocalBroadcast in now deprecated. Alternatively you can use eventbus to communication between fragments
Create a local Broadcast
private BroadcastReceiver onNotice= new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
// intent can contain anydata
Log.d(TAG,"onReceive called");
}
};
Register your receiver in onResume of fragment like:
public void onResume() {
super.onResume();
IntentFilter iff= new IntentFilter(MyIntentService.ACTION);
LocalBroadcastManager.getInstance(this).registerReceiver(onNotice, iff);
}
unRegister receiver in onPause:
public void onPause() {
super.onPause();
LocalBroadcastManager.getInstance(this).unregisterReceiver(onNotice);
}
For more information can you can refer to:
https://blog.mindorks.com/using-localbroadcastmanager-in-android
2 ) You can use LiveData to observe data changes of one fragment in another
Create shared ViewModel
public class SharedViewModel extends ViewModel {
private MutableLiveData<String> name;
public void setNameData(String nameData) {
name.setValue(nameData);
}
public MutableLiveData<String> getNameData() {
if (name == null) {
name = new MutableLiveData<>();
}
return name;
}
}
Fragment One
private SharedViewModel sharedViewModel;
public FragmentOne() {
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sharedViewModel = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
submitButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
sharedViewModel.setNameData(submitText.getText().toString());
}
});
}
Fragment Two
private SharedViewModel sharedViewModel;
public FragmentTwo() {
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sharedViewModel = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
sharedViewModel.getNameData().observe(this, nameObserver);
}
Observer<String> nameObserver = new Observer<String>() {
#Override
public void onChanged(String name) {
receivedText.setText(name);
}
};
For more details on viewmodel you can refer to :
https://nabeelj.medium.com/android-how-to-share-data-between-fragments-using-viewmodel-and-livedata-android-mvvm-9fc463af5152
https://developer.android.com/guide/fragments/communicate#fragments
Currently i am working in Kotlin android development. while writing the MVVM architecture, i am stuck with ViewModel Creation
Error : Only classes are allowed on the left hand side of a class literal
Related classes are
Detail
BaseViewModel class, while creating the object of this class, we getting the error
package com.logicipher.mvvm.ui.base
import androidx.databinding.ObservableBoolean
import androidx.lifecycle.ViewModel
import com.logicipher.mvvm.data.DataManager
import com.logicipher.mvvm.utils.rx.SchedulerProvider
import io.reactivex.disposables.CompositeDisposable
import java.lang.ref.WeakReference
/**
* Created by Shamji N S on 20-08-2020.
*/
abstract class BaseViewModel<N>(
dataManager: DataManager,
schedulerProvider: SchedulerProvider
) : ViewModel() {
private val mDataManager: DataManager
private val mIsLoading = ObservableBoolean()
private val mSchedulerProvider: SchedulerProvider
private val mCompositeDisposable: CompositeDisposable
private var mNavigator: WeakReference<N>? = null
override fun onCleared() {
mCompositeDisposable.dispose()
super.onCleared()
}
fun getCompositeDisposable(): CompositeDisposable {
return mCompositeDisposable
}
fun getDataManager(): DataManager {
return mDataManager
}
fun getIsLoading(): ObservableBoolean {
return mIsLoading
}
fun setIsLoading(isLoading: Boolean) {
mIsLoading.set(isLoading)
}
fun getNavigator(): N? {
return mNavigator!!.get()
}
fun setNavigator(navigator: N) {
mNavigator = WeakReference(navigator)
}
fun getSchedulerProvider(): SchedulerProvider {
return mSchedulerProvider
}
init {
mDataManager = dataManager
mSchedulerProvider = schedulerProvider
mCompositeDisposable = CompositeDisposable()
}
}
BaseActivity
package com.logicipher.mvvm.ui.base
import android.annotation.TargetApi
import android.app.ProgressDialog
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.inputmethod.InputMethodManager
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import com.logicipher.mvvm.utils.CommonUtils
import com.logicipher.mvvm.utils.NetworkUtils
import dagger.android.AndroidInjection
/**
* Created by Shamji N S on 20-08-2020.
*/
abstract class BaseActivity<T : ViewDataBinding?, V : BaseViewModel<*>?> :
AppCompatActivity(), BaseFragment.Callback {
// TODO
// this can probably depend on isLoading variable of BaseViewModel,
// since its going to be common for all the activities
private var mProgressDialog: ProgressDialog? = null
private var mViewDataBinding: T? = null
private var mViewModel: V? = null
/**
* Override for set binding variable
*
* #return variable id
*/
abstract fun getBindingVariable(): Int
/**
* #return layout resource id
*/
#LayoutRes
abstract fun getLayoutId(): Int
/**
* Override for set view model
*
* #return view model instance
*/
abstract fun getViewModel(): V
override fun onFragmentAttached() {
}
override fun onFragmentDetached(tag: String?) {
}
override fun onCreate(savedInstanceState: Bundle?) {
performDependencyInjection()
super.onCreate(savedInstanceState)
performDataBinding()
}
fun getViewDataBinding(): T? {
return mViewDataBinding
}
#TargetApi(Build.VERSION_CODES.M)
fun hasPermission(permission: String?): Boolean {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
checkSelfPermission(permission!!) == PackageManager.PERMISSION_GRANTED
}
fun hideKeyboard() {
val view = this.currentFocus
if (view != null) {
val imm =
getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm?.hideSoftInputFromWindow(view.windowToken, 0)
}
}
fun hideLoading() {
if (mProgressDialog != null && mProgressDialog!!.isShowing) {
mProgressDialog!!.cancel()
}
}
fun isNetworkConnected(): Boolean {
return NetworkUtils.isNetworkConnected(applicationContext)
}
fun openActivityOnTokenExpire() {
}
fun performDependencyInjection() {
AndroidInjection.inject(this)
}
#TargetApi(Build.VERSION_CODES.M)
fun requestPermissionsSafely(
permissions: Array<String?>?,
requestCode: Int
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(permissions!!, requestCode)
}
}
fun showLoading() {
hideLoading()
mProgressDialog = CommonUtils.showLoadingDialog(this)
}
private fun performDataBinding() {
mViewDataBinding = DataBindingUtil.setContentView<T>(this, getLayoutId())
mViewModel = if (mViewModel == null) getViewModel() else mViewModel
mViewDataBinding!!.setVariable(getBindingVariable(), mViewModel)
mViewDataBinding!!.executePendingBindings()
}
}
MainActivity
package com.logicipher.mvvm.ui.main
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.ViewModelProviders
import com.logicipher.mvvm.BR
import com.logicipher.mvvm.R
import com.logicipher.mvvm.ViewModelProviderFactory
import com.logicipher.mvvm.databinding.ActivityMainBinding
import com.logicipher.mvvm.ui.base.BaseActivity
import javax.inject.Inject
/**
* Created by Shamji N S on 21-08-2020.
*/
class MainActivity : BaseActivity<ActivityMainBinding?, MainViewModel<MainNavigator>?>(),
MainNavigator /*, HasSupportFragmentInjector*/ {
/* #Inject
DispatchingAndroidInjector<Fragment> fragmentDispatchingAndroidInjector;*/
#set:Inject
internal var factory: ViewModelProviderFactory? = null
var mViewModel: MainViewModel<MainNavigator>? = null
var mBinding: ActivityMainBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate: ")
mBinding = getViewDataBinding()
mViewModel?.setNavigator(this)
}
override fun getBindingVariable(): Int {
return BR.viewModel
}
override fun getLayoutId(): Int {
return R.layout.activity_main
}
override fun getViewModel(): MainViewModel<MainNavigator>? {
mViewModel =
ViewModelProviders.of(this, factory).get<MainViewModel<MainNavigator>>(MainViewModel<MainNavigator>::class.java)
return mViewModel
}
/*
#Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return fragmentDispatchingAndroidInjector;
}
*/
companion object {
private const val TAG = "MainActivity"
}
}
ViewModelProviderFactory
package com.logicipher.mvvm
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider.NewInstanceFactory
import com.logicipher.mvvm.data.DataManager
import com.logicipher.mvvm.ui.main.MainNavigator
import com.logicipher.mvvm.ui.main.MainViewModel
import com.logicipher.mvvm.utils.rx.SchedulerProvider
import java.lang.IllegalArgumentException
import javax.inject.Inject
/**
* Created by Shamji N S on 25-08-2020.
*/
public class ViewModelProviderFactory #Inject constructor(
dataManager: DataManager,
schedulerProvider: SchedulerProvider
) : NewInstanceFactory() {
private val dataManager: DataManager
private val schedulerProvider: SchedulerProvider
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return super.create(modelClass)
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel<T>(dataManager, schedulerProvider) as T
}
throw IllegalArgumentException("Unknown view model" + modelClass.name)
}
init {
this.dataManager = dataManager
this.schedulerProvider = schedulerProvider
}
}
Your BaseViewModel is abstract, you cannot create an object or an instance of an abstract class.
After you create a ViewModelFactory the way to create (or obtain) an instance of the ViewModel is this:
val viewModel = ViewModelProvider(this, viewModelFactory).get(ViewModelName::class.java)
However, I would consider using Kotlin Property Delegate from here which can help reduce the code.
I'm trying to build an app with Android Architecture Components. I'm using TMDB API in my app. In my app, a user searches for a movie or series and gets the result. I've achieved this but I want to get all the pages from API with Paging library. (Endless Recyclerview) I've looked at several tutorials but I didn't get what I wanted. Please help me, I'm new with this Android Architecture Components. Thank you in advance.
The API result:
RecyclerViewMovieAdapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.martiandeveloper.muuvi.R
import com.martiandeveloper.muuvi.model.Movie
import kotlinx.android.synthetic.main.recyclerview_movie_item.view.*
class RecyclerViewMovieAdapter(private val movieList: ArrayList<Movie>) :
RecyclerView.Adapter<RecyclerViewMovieAdapter.RecyclerViewMovieViewHolder>() {
lateinit var context: Context
class RecyclerViewMovieViewHolder(var view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerViewMovieViewHolder {
context = parent.context
val view = LayoutInflater.from(context)
.inflate(R.layout.recyclerview_movie_item, parent, false)
return RecyclerViewMovieViewHolder(view)
}
override fun getItemCount(): Int {
return movieList.size
}
override fun onBindViewHolder(holder: RecyclerViewMovieViewHolder, position: Int) {
if (movieList[position].mediaType != "person") {
if (movieList[position].mediaType == "tv") {
val title =
movieList[position].originalName + " (" + movieList[position].firstAirDate?.split(
"-"
)?.get(0) + ")"
holder.view.recyclerview_movie_item_titleMTV.text = title
} else {
val title =
movieList[position].title + " (" + movieList[position].releaseDate?.split("-")
?.get(0) + ")"
holder.view.recyclerview_movie_item_titleMTV.text = title
}
holder.view.recyclerview_movie_item_voteAverageMTV.text =
movieList[position].voteAverage.toString()
Glide.with(context)
.load("https://image.tmdb.org/t/p/w300${movieList[position].posterPath}")
.placeholder(R.drawable.logo1)
.centerCrop()
.into(holder.view.recyclerview_movie_item_posterIV)
}
}
fun updateMovieList(newMovieList: List<Movie>) {
movieList.clear()
movieList.addAll(newMovieList)
notifyDataSetChanged()
}
}
Movie.kt
import com.google.gson.annotations.SerializedName
data class Movie(
#SerializedName("original_name")
val originalName: String?,
#SerializedName("genre_ids")
val genreIds: List<Int>?,
#SerializedName("media_type")
val mediaType: String?,
#SerializedName("name")
val name: String?,
#SerializedName("origin_country")
val originCountry: List<String>?,
#SerializedName("first_air_date")
val firstAirDate: String?,
#SerializedName("original_language")
val originalLanguage: String?,
#SerializedName("id")
val id: Int?,
#SerializedName("vote_average")
val voteAverage: Float?,
#SerializedName("overview")
val overview: String?,
#SerializedName("poster_path")
val posterPath: String?,
#SerializedName("title")
val title: String?,
#SerializedName("release_date")
val releaseDate: String?,
#SerializedName("original_title")
val originalTitle: String?)
MovieResult.kt
import com.google.gson.annotations.SerializedName
data class MovieResult(
#SerializedName("page")
val page: Int?,
#SerializedName("total_results")
val totalResults: Int?,
#SerializedName("total_pages")
val totalPages: Int?,
#SerializedName("results")
val results: ArrayList<Movie>?
)
MovieApi.kt
import com.martiandeveloper.muuvi.model.MovieResult
import io.reactivex.Single
import retrofit2.http.GET
import retrofit2.http.Query
interface MovieApi {
#GET("search/multi")
fun getMovie(
#Query("api_key") apiKey: String,
#Query("query") movie: String,
#Query("page") page: Int
): Single<MovieResult>
}
MovieService.kt
import com.martiandeveloper.muuvi.model.MovieResult
import io.reactivex.Single
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
const val API_KEY = "fb640042b4bc08f6f7f65fbd8573f2a9"
const val BASE_URL = "https://api.themoviedb.org/3/"
// https://api.themoviedb.org/3/search/multi?api_key=my_api_key&query=break&page=1
// https://image.tmdb.org/t/p/w342/or06FN3Dka5tukK1e9sl16pB3iy.jpg
class MovieService {
private val api =
Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build()
.create(MovieApi::class.java)
fun getData(movie: String, page: Int): Single<MovieResult> {
return api.getMovie(API_KEY, movie, page)
}
}
AddFragment.kt
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import com.martiandeveloper.muuvi.R
import com.martiandeveloper.muuvi.adapter.RecyclerViewMovieAdapter
import com.martiandeveloper.muuvi.databinding.FragmentAddBinding
import com.martiandeveloper.muuvi.viewmodel.AddViewModel
import kotlinx.android.synthetic.main.fragment_add.*
class AddFragment : Fragment(), View.OnClickListener {
private lateinit var vm: AddViewModel
private val adapter = RecyclerViewMovieAdapter(arrayListOf())
private lateinit var binding: FragmentAddBinding
private lateinit var layoutManager: LinearLayoutManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vm = activity?.run {
ViewModelProviders.of(this)[AddViewModel::class.java]
} ?: throw Exception("Invalid activity")
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding =
DataBindingUtil.inflate(inflater, R.layout.fragment_add, container, false)
binding.addViewModel = vm
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUI()
}
private fun initUI() {
setRecyclerView()
observe()
setProgress(isRecyclerViewGone = false, isProgressLLViewGone = true)
binding.isClearIVGone = true
setListeners()
fragment_add_movieSeriesET.requestFocus()
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(fragment_add_movieSeriesET, InputMethodManager.SHOW_IMPLICIT)
}
private fun setRecyclerView() {
layoutManager = LinearLayoutManager(context)
fragment_add_mainRV.layoutManager = layoutManager
fragment_add_mainRV.adapter = adapter
}
private fun observe() {
vm.movieList.observe(viewLifecycleOwner, Observer { movieList ->
movieList?.let {
adapter.updateMovieList(it)
}
})
vm.isError.observe(viewLifecycleOwner, Observer { isError ->
isError?.let {
setProgress(isRecyclerViewGone = false, isProgressLLViewGone = true)
if (it) {
setToast(resources.getString(R.string.something_went_wrong_please_try_again_later))
}
}
})
vm.isLoading.observe(viewLifecycleOwner, Observer { isLoading ->
isLoading?.let {
if (it) {
setProgress(isRecyclerViewGone = true, isProgressLLViewGone = false)
} else {
setProgress(isRecyclerViewGone = false, isProgressLLViewGone = true)
}
}
})
vm.movieSeriesETContent.observe(viewLifecycleOwner, Observer {
if (it.isNotEmpty()) {
vm.refreshData(it, 1)
binding.isClearIVGone = false
} else {
adapter.updateMovieList(arrayListOf())
binding.isClearIVGone = true
}
})
}
private fun setProgress(isRecyclerViewGone: Boolean, isProgressLLViewGone: Boolean) {
if (vm.movieSeriesETContent.value != null) {
val text =
"${resources.getString(R.string.searching_for)} \"${vm.movieSeriesETContent.value}\"..."
binding.searchingFor = text
}
binding.isMainRVGone = isRecyclerViewGone
binding.isProgressLLGone = isProgressLLViewGone
}
private fun setToast(text: String) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
}
private fun setListeners() {
fragment_add_clearIV.setOnClickListener(this)
}
override fun onClick(v: View?) {
if (v != null) {
when (v.id) {
R.id.fragment_add_clearIV -> fragment_add_movieSeriesET.text.clear()
}
}
}
}
AddViewModel.kt
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.martiandeveloper.muuvi.model.MovieResult
import com.martiandeveloper.muuvi.model.Movie
import com.martiandeveloper.muuvi.service.MovieService
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.observers.DisposableSingleObserver
import io.reactivex.schedulers.Schedulers
class AddViewModel : ViewModel() {
private val movieService = MovieService()
private val disposable = CompositeDisposable()
val movieList = MutableLiveData<ArrayList<Movie>>()
val isError = MutableLiveData<Boolean>()
val isLoading = MutableLiveData<Boolean>()
val movieSeriesETContent = MutableLiveData<String>()
fun refreshData(movie: String, page: Int) {
isLoading.value = true
disposable.add(
movieService.getData(movie, page).subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableSingleObserver<MovieResult>() {
override fun onSuccess(t: MovieResult) {
movieList.value = t.results
isError.value = false
isLoading.value = false
}
override fun onError(e: Throwable) {
isError.value = true
isLoading.value = false
}
})
)
}
}