Observer pattern is not working in Android MVVM - kotlin

I am trying to update my view according to my data in my ViewModel, using MVVM
I need in the method onCacheReception to update my map whenever zones is changing
ViewModel
class MainViewModel constructor(application: Application) : AndroidViewModel(application),
CacheListener {
private val instance = Initializer.getInstance(application.applicationContext)
private val _zones = MutableLiveData<List<Zone>>()
val zones: LiveData<List<Zone>>
get() = _zones
init {
CacheDispatcher.addCacheListener(this)
}
override fun onCacheReception() {
val zonesFromDB: List<Zone>? = instance.fetchZonesInDatabase()
_zones.value = zonesFromDB
}
}
MainActivity
class MainActivity : AppCompatActivity(), EasyPermissions.PermissionCallbacks, OnMapReadyCallback {
private val mainViewModel: MainViewModel = ViewModelProvider(this).get(MainViewModel(application)::class.java)
private lateinit var initializer: Initializer
private lateinit var map: GoogleMap
private val REQUEST_CODE_LOCATIONS: Int = 100
private val permissionLocationsRationale: String = "Permissions for Fine & Coarse Locations"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (checkForLocationsPermission()) {
setUp()
mapSetUp()
}
mainViewModel.zones.observe(this, Observer { zones ->
zones.forEach {
Log.i("YES DATA", "Data has been updated")
val latLng = it.lat?.let { it1 -> it.lng?.let { it2 -> LatLng(it1, it2) } }
val markerOptions = latLng?.let { it1 -> MarkerOptions().position(it1) }
map.addMarker(markerOptions)
}
})
}
My Log is never displaying and it doesn't seem while debugging that mainView.zones.observe { } is called when I receive some new data in my ViewModel

In the onCacheReception(), replace:
_zones.value = zonesFromDB
by:
_zones.postValue(zonesFromDB)
in case your onCacheReception() function is called from a worker thread.

Related

Getting list from viewmodel in observe event -MVVM

I have a issue in getting a list returned in observe event in my activity. i am developing a login screen in MVVM. viewmodel is as follows.
my problem is i can get returned data in observe call back into a UI control. but same data returned assign into a list variable is empty. in other words, list returned unable to pass into a a list variable in an activity.
class LoginViewModel #Inject internal constructor (private val loginRepository: LoginRepository,private val usersRepository: UsersRepository): ViewModel() {
private var _userEmail:MutableLiveData<String>
private var _userPassword:MutableLiveData<String>
private var _userLoginData:MutableLiveData<UserLoginData>
private var allUsers:MutableLiveData<List<Users>>
private var findUser:MutableLiveData<List<Users>>
init{
_userEmail= MutableLiveData()
_userPassword= MutableLiveData()
_userLoginData= MutableLiveData()
allUsers= MutableLiveData()
findUser= MutableLiveData()
}
fun getEmail():LiveData<String>{
return _userEmail
}
fun getPassword():MutableLiveData<String>{
return _userPassword
}
fun userLogin(userEmail:String,userPassword:String):MutableLiveData<UserLoginData>{
_userEmail.postValue(userEmail)
_userPassword.postValue(userPassword)
viewModelScope.launch(Dispatchers.IO) {
var userlogindata:UserLoginData=loginRepository.userLogin(userEmail,userPassword)
_userLoginData.postValue(userlogindata)
}
return _userLoginData
}
fun getAllUsers():MutableLiveData<List<Users>>{
//lateinit var _allUsers:List<Users>
viewModelScope.launch(Dispatchers.IO) {
val _allUsers:List<Users> =usersRepository.getUsers()
allUsers.postValue(_allUsers)
}
return allUsers
}
fun findUser(userEmail:String):MutableLiveData<List<Users>>{
//lateinit var finduser:List<Users>
viewModelScope.launch(Dispatchers.IO) {
val _findUser:List<Users> =usersRepository.findUser(userEmail)
findUser.postValue(_findUser)
}
return findUser
}
}
in an activity i am observing the users list and getting the list into a list variable in the activity. code in the activity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var loginViewModel: LoginViewModel
lateinit var loginData:UserLoginData
var users:List<Users> = emptyList()
var findUser:List<Users> = emptyList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
loginViewModel = ViewModelProvider(this).get(LoginViewModel::class.java)
/*observe users list*/
loginViewModel.getAllUsers().observe(this, {It->
users=It
binding.textView.text=It[0].email.toString()
})
loginViewModel.findUser(binding.loginEditTextTextEmailAddressTxt.toString().trim()).observe(this,{it->
findUser=it
})
This program failed if i use data in the users or findUser lists.
Kindly help me to find the best practice in getting the changed data from viewmodel into an activity
ViewModel:
data class User(
var name: String
)
private val _allUsers = MutableLiveData<List<User>>()
private val allUsers: LiveData<List<User>> get() = _allUsers
fun fetchAllUsers(): LiveData<List<User>> {
viewModelScope.launch {
//delay is simulating network request delay
delay(1000)
//listOf is simulating usersRepository.getUsers()
_allUsers.value = listOf(User("name1"), User("name2"), User("name3"))
}
return allUsers
}
Fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.fetchAllUsers().observe(viewLifecycleOwner) { userList ->
userList.forEach {
Log.d("user", it.name)
}
}
You can try this way but I do not prefer returning liveData with function because you have to observe liveData once. You need to be careful observe once.

Obtain a crash message-App keeps stopping

I get the following trace error java.lang.ClassCastException: com.example.myhouse.MainActivity cannot be cast to com.brsthegck.kanbanboard.TasklistFragment$Callbacks at com.brsthegck.kanbanboard.TasklistFragment.onAttach(TasklistFragment.kt:52)at androidx.fragment.app.Fragment.performAttach(Fragment.java:2672) at androidx.fragment.app.FragmentStateManager.attach(FragmentStateManager.java:263) at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1170) at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1356)
enter image description here
Tasklist Fragment
private const val ARG_TASKLIST_TYPE = "tasklist_type"
private const val TASKLIST_TYPE_TODO = 0
private const val TASKLIST_TYPE_DOING = 1
private const val TASKLIST_TYPE_DONE = 2
class TasklistFragment : Fragment(){
private lateinit var visibleColorPaletteViewList : List<View>
lateinit var taskRecyclerView: RecyclerView
private var tasklistType : Int = -1
private var adapter : TaskViewAdapter? = TaskViewAdapter(LinkedList<Task>())
private var colorPaletteIsVisible : Boolean = false
private var callbacks: Callbacks? = null
//Callback interface to delegate access functions in MainActivity
interface Callbacks{
fun addTaskToViewModel(task: Task, destinationTasklistType: Int)
fun deleteTaskFromViewModel(tasklistType: Int, adapterPosition: Int)
fun getTaskListFromViewModel(tasklistType: Int) : LinkedList<Task>
}
//Attach context as a Callbacks reference to the callbacks variable when fragment attaches to container
override fun onAttach(context: Context) {
super.onAttach(context)
callbacks = activity as Callbacks? // error is here
}
//Detach context (assign to null) when fragment detaches from container
override fun onDetach() {
super.onDetach()
callbacks = null
}
//ItemTouchHelper instance with custom callback, to move task card view positions on hold
private val itemTouchHelper by lazy{
val taskItemTouchCallback = object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0){
override fun onMove(recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder): Boolean {
val adapter = recyclerView.adapter as TaskViewAdapter
val from = viewHolder.adapterPosition
val to = target.adapterPosition
adapter.moveTaskView(from, to)
adapter.notifyItemMoved(from, to)
return true
}
//Make taskview transparent while being moved
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if(actionState == ACTION_STATE_DRAG)
viewHolder?.itemView?.alpha = 0.7f
}
//Make taskview opaque while being
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.alpha = 1.0f
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { /* Not implemented on purpose. */ }
}
ItemTouchHelper(taskItemTouchCallback)
}
//Get the fragment arguments, tasklist type to be precise, and assign it to the member of this fragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
tasklistType = arguments?.getInt(ARG_TASKLIST_TYPE) as Int
}
//Inflate view of fragment, prepare recycler view and it's layout manager, and update the UI
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View?{
//Inflate the layout of this fragment, get recyclerview ref, set layout manager for recycler view
val view = inflater.inflate(R.layout.tasklist_fragment_layout, container,false)
taskRecyclerView = view.findViewById(R.id.task_recycler_view) as RecyclerView
taskRecyclerView.layoutManager = LinearLayoutManager(context)
itemTouchHelper.attachToRecyclerView(taskRecyclerView)
//Fill the recyclerview with data from viewmodel
updateInterface()
//Return the created view
return view
}
//Populate recyclerview and set up its adapter
private fun updateInterface(){
val tasks = callbacks!!.getTaskListFromViewModel(tasklistType)
adapter = TaskViewAdapter(tasks)
taskRecyclerView.adapter = adapter
}
Kanban class
class Kanban : Fragment(), TasklistFragment.Callbacks{
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
private lateinit var viewPager: ViewPager2
private lateinit var tabLayout: TabLayout
private lateinit var taskViewModel: TaskViewModel
//When add action bar button is pressed
override fun onOptionsItemSelected(item: MenuItem) = when(item.itemId) {
R.id.action_new_task -> {
val currentTasklistType = viewPager.currentItem
val currentTasklist = when (currentTasklistType) {
TASKLIST_TYPE_TODO -> taskViewModel.todoTaskList
TASKLIST_TYPE_DOING -> taskViewModel.doingTaskList
TASKLIST_TYPE_DONE -> taskViewModel.doneTaskList
else -> throw Exception("Unrecognized tasklist type")
}
val currentFragment = (activity?.supportFragmentManager?.fragments?.get(currentTasklistType) as TasklistFragment)
addTaskToViewModel(Task(), currentTasklistType)
currentFragment.taskRecyclerView.scrollToPosition(currentTasklist.size - 1)
true
}
else -> super.onOptionsItemSelected(item)
}
//Inflate the action bar menu resource on options menu creation
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_action_bar, menu)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
//Get viewmodel
val view: View = inflater.inflate(R.layout.fragment_kanban2, container, false)
taskViewModel = ViewModelProvider(this).get(TaskViewModel::class.java)
//Read tasklist data from shared prefs and populate viewmodel lists with it
readSharedPrefsToViewModel()
//Get ref to viewpager and set up its adapter & attributes
viewPager = view.findViewById(R.id.view_pager)
val viewPagerAdapter = TasklistFragmentStateAdapter(this)
viewPager.apply {
adapter = viewPagerAdapter
offscreenPageLimit = 2
//Get ref to tablayout, set up & attach its mediator
tabLayout = view.findViewById(R.id.tab_layout)
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) {
0 -> getString(R.string.tab_label_todo)
1 -> getString(R.string.tab_label_doing)
else -> getString(R.string.tab_label_done)
}
}.attach()
}
return view
}
override fun onStop() {
super.onStop()
writeViewModelToSharedPrefs()
}
//Adapter class for view pager
private inner class TasklistFragmentStateAdapter(fa: Kanban) : FragmentStateAdapter(fa){
override fun createFragment(position: Int): Fragment {
//Create argument bundle with task list type
val tasklistFragmentArguments = Bundle().apply{
putInt(ARG_TASKLIST_TYPE, position)
}
//Attach the argument bundle to new fragment instance and return the fragment
return TasklistFragment().apply{
arguments = tasklistFragmentArguments
}
}
override fun getItemCount(): Int = NUM_TASKLIST_PAGES
}
override fun addTaskToViewModel(task: Task, destinationTasklistType: Int) {
val destinationFragment = (activity?.supportFragmentManager?.fragments?.get(destinationTasklistType) as TasklistFragment)
val taskList = getTaskListFromViewModel(destinationTasklistType)
taskList.add(task)
destinationFragment.taskRecyclerView.adapter?.notifyItemInserted(taskList.size)
}
override fun deleteTaskFromViewModel(tasklistType: Int, adapterPosition: Int) {
val tasklistFragment = (activity?.supportFragmentManager?.fragments?.get(tasklistType) as TasklistFragment)
getTaskListFromViewModel(tasklistType).removeAt(adapterPosition)
tasklistFragment.taskRecyclerView.adapter?.notifyItemRemoved(adapterPosition)
}
override fun getTaskListFromViewModel(tasklistType: Int): LinkedList<Task> =
when(tasklistType){
TASKLIST_TYPE_TODO -> taskViewModel.todoTaskList
TASKLIST_TYPE_DOING -> taskViewModel.doingTaskList
TASKLIST_TYPE_DONE -> taskViewModel.doneTaskList
else -> throw Exception("Unrecognized tasklist type") }
private fun writeViewModelToSharedPrefs(){
val gson = Gson()
//Convert list to json string
val todoJSON = gson.toJson(taskViewModel.todoTaskList)
val doingJSON = gson.toJson(taskViewModel.doingTaskList)
val doneJSON = gson.toJson(taskViewModel.doneTaskList)
var sharedPref : SharedPreferences = requireActivity().getPreferences(Context.MODE_PRIVATE);
//Save json strings into shared preferences
activity?.getPreferences(MODE_PRIVATE)?.edit()?.apply {
putString(KEY_TODO_JSON, todoJSON)
putString(KEY_DOING_JSON, doingJSON)
putString(KEY_DONE_JSON, doneJSON)
}?.apply()
}
private fun readSharedPrefsToViewModel(){
val gson = Gson()
val sharedPrefs = activity?.getPreferences(MODE_PRIVATE)
val todoJSON = sharedPrefs?.getString(KEY_TODO_JSON, "[]")
val doingJSON = sharedPrefs?.getString(KEY_DOING_JSON, "[]")
val doneJSON = sharedPrefs?.getString(KEY_DONE_JSON, "[]")
val type = object: TypeToken<LinkedList<Task>>() {}.type //Gson requires type ref for generic types
taskViewModel.todoTaskList = gson.fromJson(todoJSON, type)
taskViewModel.doingTaskList = gson.fromJson(doingJSON, type)
taskViewModel.doneTaskList = gson.fromJson(doneJSON, type)
}
Main activity
public class MainActivity extends AppCompatActivity {
private TabLayout tabLayout;
private ViewPager2 viewPager2;
private MyFragment adapter;
private TextView register;
GridView contractorsGV;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tabLayout = findViewById(R.id.tabLayout);
viewPager2 = findViewById(R.id.viewPager2);
tabLayout.addTab(tabLayout.newTab().setText("Home"));
tabLayout.addTab(tabLayout.newTab().setText("Idea Book"));
tabLayout.addTab(tabLayout.newTab().setText("My Profile"));
FragmentManager fragmentManager = getSupportFragmentManager();
adapter = new MyFragment(fragmentManager , getLifecycle());
viewPager2.setAdapter(adapter);
4. Fragment manager
public class MyFragment extends FragmentStateAdapter {
public MyFragment(#NonNull FragmentManager fm,Lifecycle lifecycle) {
super(fm,lifecycle);
}
#NonNull
#Override
public Fragment createFragment(int position) {
if(position==1)
{
return new Kanban();
}
if(position==2) {
return new signOut();
}
return new Navigation_Bar();
}

How to get data from Request<String> in other class using Vollely library,

I am trying to write simple httpclient, and i am trying to figure out how to get data from response. I have two classess:
class HttpClient(private val ctx: Context) {
private var queue: RequestQueue = Volley.newRequestQueue(this.ctx)
private lateinit var gpsModel: GpsModel
companion object {
const val BASE_URL: String = "https://uawk2yh00j.execute-api.eu-central-1.amazonaws.com/"
}
fun getGpsData() : GpsModel
{
this.queue.add(
StringRequest(
Request.Method.GET, BASE_URL + "production",
{
Log.d("GET_ALL_DATA", it.toString())
Log.d("GET_ALL_DATA", GpsModel.fromJson(it.toString())?.body?.size.toString())
this.gpsModel = GpsModel.fromJson(it.toString())!!
Log.d("GET_ALL_DATA", this.gpsModel.toString())
},
{
Log.d("GET_ALL_DATA", "Coś poszło nie tak")
}
)
)
return this.gpsModel
}
}
and main activity:
class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var mMap: GoogleMap
private lateinit var httpClient: HttpClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_maps)
this.httpClient = HttpClient(this)
var gpsModel = this.httpClient.getGpsData()
Log.d("GET_ALL_DATA", gpsModel.toString())
val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
}
override fun onMapReady(googleMap: GoogleMap) {
}
}
And i would like to know when Request is successfull and when is error, but i dont see any onSuccess or onFailure methods.
I found the answer: Its about creating interface. The link is here: Android Volley - How to isolate requests in another class

Listening to coroutine from view cant be done from the views init

I am trying to listen to my ViewModels MutableStateFlow from my FlutterSceneView. But I get the following error when trying to set the listener from the views init:
Suspend function 'listenToBackgroundColor' should be called only from a coroutine or another suspend function
class FlutterSceneView(context: Context, private val viewModel: FlutterSceneViewModelType): PlatformView {
private val context = context
private val sceneView = SceneView(context)
init {
listenToBackgroundColor() // Error here
}
private suspend fun listenToBackgroundColor() {
viewModel.colorFlow.collect {
val newColor = Color.parseColor(it)
sceneView.setBackgroundColor(newColor)
}
}
}
My ViewModel:
interface FlutterSceneViewModelType {
var colorFlow: MutableStateFlow<String>
}
class FlutterSceneViewModel(private val database: Database): FlutterSceneViewModelType, ViewModel() {
override var colorFlow = MutableStateFlow<String>("#FFFFFF")
init {
listenToBackgroundColorFlow()
}
private fun listenToBackgroundColorFlow() {
database.backgroundColorFlow.watch {
colorFlow.value = it.hex
}
}
}
the .watch call is a helper I have added so that this can be exposed to iOS using Kotlin multi-platform, it looks as follows but I can use collect instead if necessary:
fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun watch(block: (T) -> Unit): Closeable {
val job = Job()
onEach {
block(it)
}.launchIn(CoroutineScope(Dispatchers.Main + job))
return object : Closeable {
override fun close() {
job.cancel()
}
}
}
}
I resolved this by using viewModel context:
private fun listenToBackgroundColor() {
viewModel.colorFlow.onEach {
val newColor = Color.parseColor(it)
sceneView.setBackgroundColor(newColor)
}.launchIn(viewModel.viewModelScope)
}
I had to import the following into my ViewModel:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
from:
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0")

Test a view model with livedata, coroutines (Kotlin)

I've been trying to test my view model for several days without success.
This is my view model :
class AdvertViewModel : ViewModel() {
private val parentJob = Job()
private val coroutineContext: CoroutineContext
get() = parentJob + Dispatchers.Default
private val scope = CoroutineScope(coroutineContext)
private val repository : AdvertRepository = AdvertRepository(ApiFactory.Apifactory.advertService)
val advertContactLiveData = MutableLiveData<String>()
fun fetchRequestContact(requestContact: RequestContact) {
scope.launch {
val advertContact = repository.requestContact(requestContact)
advertContactLiveData.postValue(advertContact)
}
}
}
This is my repository :
class AdvertRepository (private val api : AdvertService) : BaseRepository() {
suspend fun requestContact(requestContact: RequestContact) : String? {
val advertResponse = safeApiCall(
call = {api.requestContact(requestContact).await()},
errorMessage = "Error Request Contact"
)
return advertResponse
}
}
This is my view model test :
#RunWith(JUnit4::class)
class AdvertViewModelTest {
private val goodContact = RequestContact(...)
private lateinit var advertViewModel: AdvertViewModel
private var observer: Observer<String> = mock()
#get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
#Before
fun setUp() {
advertViewModel = AdvertViewModel()
advertViewModel.advertContactLiveData.observeForever(observer)
}
#Test
fun fetchRequestContact_goodResponse() {
advertViewModel.fetchRequestContact(goodContact)
val captor = ArgumentCaptor.forClass(String::class.java)
captor.run {
verify(observer, times(1)).onChanged(capture())
assertEquals("someValue", value)
}
}
}
The method mock() :
inline fun <reified T> mock(): T = Mockito.mock(T::class.java)
I got this error :
Wanted but not invoked: observer.onChanged();
-> at com.vizzit.AdvertViewModelTest.fetchRequestContact_goodResponse(AdvertViewModelTest.kt:52)
Actually, there were zero interactions with this mock.
I don't understand how to retrieve the result of my query.
You would need to write a OneTimeObserver to observe livedata from the ViewModel
class OneTimeObserver<T>(private val handler: (T) -> Unit) : Observer<T>, LifecycleOwner {
private val lifecycle = LifecycleRegistry(this)
init {
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
override fun getLifecycle(): Lifecycle = lifecycle
override fun onChanged(t: T) {
handler(t)
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
}
After that you can write an extension function:
fun <T> LiveData<T>.observeOnce(onChangeHandler: (T) -> Unit) {
val observer = OneTimeObserver(handler = onChangeHandler)
observe(observer, observer)
}
Than you can check this ViewModel class class that I have from a project to check what's going on with your LiveData after you act (when) with invoking a method.
As for your error, it just says that the onChanged() method is not being called ever.