Unresolved reference (vars) in Fragment class, MVVM, Viewmodel - android

I'm following UDACITY free course on developing Android apps with Kotlin and I'm actually at the Viewmodel/MVVM part of the lesson, ie implementing Viewmodel classes for a better separation of concerns. So anyway, Im blocked right now. The exercise I'm on is about creating the Viewmodel class and transferring variables and functions from the Fragment class to this newly created class. I follow the tutorial step by step, check the correct answer on the provided git diff and I still find myself blocked by Unresolved reference errors.
Before changing the code, i had to update my Gradle module file to use ViewModel
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
Then I had to declare my Viewmodel object in my Fragment class
gameViewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
ViewModelProviders being deprecated, old course, I had to change, after search, to
gameViewModel = ViewModelProvider(this).get(GameViewModel::class.java)
It seems to be the right way to do, but I'm still left with some Unresolved references on the variables word (gameViewModel.word) and score (gameViewModel.score) in the Fragment class, unable to compile. I dont know if I declared the Viewmodel object correctly or if I'm missing something else...
I dont have this problem, Unresolved reference, with my ViewModel class functions, ie gameViewModel.onCorrect() and gameViewModel.onSkip(). They seem to be properly declared and called in the Fragment class, which begs me the question on my variables, word and score...
My Fragment class
package com.example.android.guesstheword.screens.game
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.NavHostFragment.findNavController
import com.example.android.guesstheword.R
import com.example.android.guesstheword.databinding.GameFragmentBinding
import timber.log.Timber
class GameFragment : Fragment() {
private lateinit var binding: GameFragmentBinding
private lateinit var gameViewModel: GameViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate view and obtain an instance of the binding class
binding = DataBindingUtil.inflate(
inflater,
R.layout.game_fragment,
container,
false
)
Timber.i("onCreateView GameFragment called")
gameViewModel = ViewModelProvider(this).get(GameViewModel::class.java)
Timber.i("ViewModelProvider is called")
binding.correctButton.setOnClickListener {
gameViewModel.onCorrect()
updateScoreText()
updateWordText()
}
binding.skipButton.setOnClickListener {
gameViewModel.onSkip()
updateScoreText()
updateWordText()
}
updateScoreText()
updateWordText()
return binding.root
}
/**
* Called when the game is finished
*/
fun gameFinished() {
val action = GameFragmentDirections.actionGameToScore(gameViewModel.score)
findNavController(this).navigate(action)
}
/** Methods for updating the UI **/
private fun updateWordText() {
binding.wordText.text = gameViewModel.word
}
private fun updateScoreText() {
binding.scoreText.text = gameViewModel.score.toString()
}
override fun onDestroyView() {
super.onDestroyView()
Timber.i("onDestroyView GameFragment called")
}
}
My ViewModel class
package com.example.android.guesstheword.screens.game
import androidx.lifecycle.ViewModel
import timber.log.Timber
var word = ""
var score = 0
private lateinit var wordList: MutableList<String>
class GameViewModel: ViewModel() {
init {
Timber.i("GameViewModel is created")
resetList()
nextWord()
}
override fun onCleared() {
super.onCleared()
Timber.i("GameViewModel is cleared")
}
/**
* Resets the list of words and randomizes the order
*/
fun resetList() {
wordList = mutableListOf(
"queen",
"hospital",
"basketball",
"cat",
"change",
"snail",
"soup",
"calendar",
"sad",
"desk",
"guitar",
"home",
"railway",
"zebra",
"jelly",
"car",
"crow",
"trade",
"bag",
"roll",
"bubble"
)
wordList.shuffle()
}
/**
* Moves to the next word in the list
*/
private fun nextWord() {
//Select and remove a word from the list
if (wordList.isEmpty()) {
//gameFinished()
} else {
word = wordList.removeAt(0)
}
}
/** Methods for buttons presses **/
fun onSkip() {
score--
nextWord()
}
fun onCorrect() {
score++
nextWord()
}
}
Where did I screw up ?

The variables you're trying to access aren't part of the same scope:
var word = ""
var score = 0
private lateinit var wordList: MutableList<String>
class GameViewModel: ViewModel() {
...
your variables are declared outside of the ViewModel, add them inside the class GameViewModel to make them instance variables:
class GameViewModel: ViewModel() {
var word = ""
var score = 0
...
}

Related

Can I save the state of my viewModel that holds data for my fragments?

I am new to Android app dev,I want my fragment to store user information in a viewModel, yet that model is accessed by other fragments. The app runs well but I would like it to store the viewModel even if the app is destroyed. How can I save the state of my model?
This is my viewModel class (it starts with the package name):
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
/**
* This model is shared by related views.
* stores user name1 and name2 that prevents repetition in filling forms.
* stores data lost during configuration changes.
* stores data from the form.
* */
class SharedViewModel() : ViewModel() {
private val _nameOne = MutableLiveData<String>()
val nameOne: LiveData<String> = _nameOne
private val _nameTwo = MutableLiveData<String>()
val nameTwo: LiveData<String> = _nameTwo
private val _emailAddress = MutableLiveData<String>()
val emailAddress: LiveData<String> = _emailAddress
private val _phoneNumber = MutableLiveData<String>()
val phoneNumber: LiveData<String> = _phoneNumber
private val _nationalIdNumber = MutableLiveData<String>()
val nationalIdNumber: LiveData<String> = _nationalIdNumber
fun saveUserData(
nameA: String, nameB: String,
emailA: String, phoneN: String,
natId: String) {
_nameOne.value = nameA
_nameTwo.value = nameB
_emailAddress.value = emailA
_phoneNumber.value = phoneN
_nationalIdNumber.value = natId
}
}
This is the MainActivity class (it starts with the package name):
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
import ke.co.qaizen.qaizencarrental.databinding.ActivityMainBinding
/**
* Main Activity and entry point for the app.
*/
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Thread.sleep(3)
installSplashScreen()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Get the navigation host fragment from this Activity
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
// Instantiate the navController using the NavHostFragment
navController = navHostFragment.navController
// Make sure actions in the ActionBar get propagated to the NavController
setupActionBarWithNavController(navController)
}
/**
* Enables back button support. Simply navigates one element up on the stack.
*/
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
And this is my fragment class (it also starts with the package name):
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import ke.co.qaizen.qaizencarrental.databinding.FragmentAccountBinding
import ke.co.qaizen.qaizencarrental.mainmodel.SharedViewModel
class AccountFragment : Fragment() {
private var _binding: FragmentAccountBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private val sharedViewModel: SharedViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAccountBinding.inflate(inflater, container, false)
sharedViewModel.nameOne.observe(viewLifecycleOwner) { nameOne ->
binding.userNameOne.text = nameOne
}
sharedViewModel.nameTwo.observe(viewLifecycleOwner) { nameTwo ->
binding.userNameTwo.text = nameTwo
}
sharedViewModel.emailAddress.observe(viewLifecycleOwner) { emailAddress ->
binding.userEmailAddress.text = emailAddress
}
sharedViewModel.phoneNumber.observe(viewLifecycleOwner) { phoneNumber ->
binding.userPhoneNumber.text = phoneNumber
}
sharedViewModel.nationalIdNumber.observe(viewLifecycleOwner) {
nationalIdNumber ->
binding.userIdNumber.text = nationalIdNumber
}
return binding.root
}
}
Try the Google Room DB library.
Easy store objects.

How to pass data from adapter to fragment?

I've been trying to pass data(the email and phone of a user) from my adapter to my fragment. From what I've read online I should use a interface for this but I cant the data into my fragment still. Can anyone explain in steps how I should add a interface and how to put data into my interface from my adapter so I can call it in my fragment. Or is there another way to pass data from my adapter to my fragment. Below are my adapter and my fragment.
Adapter:
package ie.wit.savvytutor.adapters
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import ie.wit.savvytutor.R
import ie.wit.savvytutor.activity.MainActivity
import ie.wit.savvytutor.fragments.ViewChatFragment
import ie.wit.savvytutor.models.UserModel
class UserAdapter(private val userList: ArrayList<UserModel>, val context: Context) :
RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val itemView =
LayoutInflater.from(parent.context).inflate(R.layout.user_layout, parent, false)
return UserViewHolder(itemView)
}
class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val username: TextView = itemView.findViewById(R.id.userNameView)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int, ) {
val currentItem = userList[position]
holder.username.text = currentItem.email
holder.itemView.setOnClickListener {
println(currentItem)
val optionsFrag = ViewChatFragment()
(context as MainActivity).getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container, optionsFrag, "OptionsFragment").addToBackStack(
null
)
.commit()
}
}
override fun getItemCount(): Int {
return userList.size
}
}
Fragment
package ie.wit.savvytutor.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.Nullable
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.*
import ie.wit.savvytutor.R
import ie.wit.savvytutor.adapters.UserAdapter
import ie.wit.savvytutor.models.UserModel
class TutorChatFragment : Fragment() {
private lateinit var userRecyclerView: RecyclerView
private lateinit var userArrayList: ArrayList<UserModel>
private lateinit var dbRef: DatabaseReference
private lateinit var mAuth: FirebaseAuth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
dbRef = FirebaseDatabase.getInstance("DATABASE LINK").getReference("Users").ref
mAuth = FirebaseAuth.getInstance()
}
#Nullable
override fun onCreateView(
inflater: LayoutInflater,
#Nullable container: ViewGroup?,
#Nullable savedInstanceState: Bundle?
): View {
//inflate the fragment layout
val root = inflater.inflate(R.layout.tutor_chat_fragment, container, false)
userRecyclerView = root.findViewById(R.id.userListView)
userRecyclerView.layoutManager = LinearLayoutManager(context)
userRecyclerView.setHasFixedSize(true)
userArrayList = arrayListOf<UserModel>()
getUser()
return root
}
private fun getUser() {
userArrayList.clear()
dbRef.addValueEventListener(object: ValueEventListener{
override fun onDataChange(snapshot: DataSnapshot) {
for (postSnapshot in snapshot.children) {
val currentUser = postSnapshot.getValue(UserModel::class.java)
//BUG FIX 1.26.13
val email = currentUser?.email
if (email != null) {
userArrayList.add(currentUser)
}
userRecyclerView.adapter?.notifyDataSetChanged()
userRecyclerView.adapter = context?.let { UserAdapter(userArrayList, it) }
}
}
override fun onCancelled(error: DatabaseError) {
TODO("Not yet implemented")
}
})
}
}
If you want to use an interface, you just need to define one with a function to receive your data, make the fragment implement it, then pass the fragment to the adapter as an implementation of that interface:
data class UserData(val email: String, val phone: String)
class UserAdapter(
private val userList: ArrayList<UserModel>,
val context: Context,
val handler: UserAdapter.Callbacks // added this here, so you're passing it in at construction
) : RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
...
private fun doWhatever(email: String, phone: String) {
// pass the data to the handler (which will probably be your Fragment)
handler.handleUserData(UserData(email, phone))
}
// nested inside the UserAdapter class to keep things tidy
interface Callbacks {
fun handleUserData(data: UserData)
}
}
Then in the Fragment:
// add the Callbacks interface type
class TutorChatFragment : Fragment(), UserAdapter.Callbacks {
override fun onCreateView(
inflater: LayoutInflater,
#Nullable container: ViewGroup?,
#Nullable savedInstanceState: Bundle?
): View {
...
userRecyclerView.layoutManager = LinearLayoutManager(context)
// set up the adapter here, passing this fragment as the Callbacks handler
userRecyclerView.adapter = UserAdapter(userArrayList, context, this)
...
}
// interface implementation
override fun handleUserData(data: UserData) {
// whatever
}
}
And that's it. You're not hardcoding a dependency on that particular Fragment type, just the interface, and this fragment implements it so it can pass itself.
A more Kotliny way to do it is to ignore interfaces and just pass a function instead
class UserAdapter(
private val userList: ArrayList<UserModel>,
val context: Context,
val handler: (UserData) -> Unit // passing a function that takes a UserData instead
) : RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
...
private fun doWhatever(email: String, phone: String) {
// call the handler function with your data (you can write handler.invoke() if you prefer)
handler(UserData(email, phone))
}
}
// no interface this time
class TutorChatFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
#Nullable container: ViewGroup?,
#Nullable savedInstanceState: Bundle?
): View {
...
userRecyclerView.layoutManager = LinearLayoutManager(context)
// pass in a handler function
userRecyclerView.adapter = UserAdapter(userArrayList, context) { userData ->
handleUserData(userData)
}
// or if you're just passing it to that function down there,
// you could do UserAdapter(userArrayList, context, ::handleUserData)
// and pass the function reference
...
}
// might be convenient to still do this in its own function
private fun handleUserData(data: UserData) {
// whatever
}
}
Ideally you should be doing what I've done there - create the adapter once during setup, and have a function on it that allows you to update it. Your code creates a new one each time you get data. You do this the same way in both though
Your other option is using a view model that the adapter and fragment both have access to, but this is how you do the interface/callback approach
Actually there is one very easy way to get data from your adapter in to your fragment or activity. It is called using Higher Order Functions.
In your adapter
Add higher order function in your adapter.
class UserAdapter(private val userList: ArrayList<UserModel>, val context: Context) :
RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
//your rest of the adapter's code
private var onItemClickListener:((UserModel)->Unit)? = null
fun setOnItemClickListener(listener: (UserModel)->Unit) {
onItemClickListener = listener
}
}
In Your UserViewHolder
val rootView = itemView.rootView
In Your onBindViewHolder
set a click listener on rootView
holder.rootView.setOnClickListener {
onItemClickListener?.let{
it(currentItem)
}
}
In Your Fragment
//create the instance of UserAdapter
userAdapter.setOnItemClickListener {
//here you have your UserModel in your fragment, do whatever you want to with it
}
And, a suggestion in the last. Start using ViewBinding, it will save you from a lots of hectic work.

Combine Kotlin code of 2 fragments into one class

I have a Fragment that I would Like to have twice in the App, but with slight changes. Is there a way to use some kind of Abstract Class? I already tried to find a solution my self, but I cant find a way to get the Viewmodel from the Activity using delegated properties as Android Studio is saying the can't be abstract. Another problem that I'm facing is the Arguments I'm passing to the Fragment.
(To clarify: The Property that should change is the Viewmodel; I'd like to have another kind of Viewmodel for the second Fragment. Also, the Viewmodel I use in the Code below and the other Viewmodel both inherit from the same class, so switching the Type shouldn't be that big of a problem)
Here's the Code of the Fragment:
package net.informatikag.thomapp.viewables.fragments.ThomsLine.main
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.android.volley.*
import com.android.volley.toolbox.JsonArrayRequest
import com.android.volley.toolbox.Volley
import com.google.android.material.snackbar.Snackbar
import net.informatikag.thomapp.MainActivity
import net.informatikag.thomapp.R
import net.informatikag.thomapp.databinding.ThomslineMainFragmentBinding
import net.informatikag.thomapp.utils.handlers.WordpressRecyclerAdapter
import net.informatikag.thomapp.utils.ArticleListSpacingDecoration
import net.informatikag.thomapp.utils.models.ArticleClickHandler
import net.informatikag.thomapp.utils.models.data.WordpressArticle
import net.informatikag.thomapp.utils.models.data.WordpressPage
import net.informatikag.thomapp.utils.models.view.ThomsLineViewModel
import java.util.*
import kotlin.collections.ArrayList
/**
* Pulls a list of articles from the JSON API of the Wordpress instance of the ThomsLine student newspaper.
* The articles are dynamically loaded with a RecyclerView.
*/
class ThomsLineFragment : Fragment(), SwipeRefreshLayout.OnRefreshListener, ArticleClickHandler {
private var _binding: ThomslineMainFragmentBinding? = null // Verweis zum Layout
private val viewModel: ThomsLineViewModel by activityViewModels() // Das Viewmodel in dem die wichtigen Daten des Fragments gespeichert werden
private lateinit var recyclerAdapter: WordpressRecyclerAdapter // Hier werden die Artikel angezeigt
private lateinit var swipeRefreshLayout: SwipeRefreshLayout // wird benutz um die Artikel neu zu laden
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
/**
* Will be executed when the fragment is opened
*/
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate Layout
_binding = ThomslineMainFragmentBinding.inflate(inflater, container, false)
val root: View = binding.root
//Instantiate Variables
recyclerAdapter = WordpressRecyclerAdapter(this, viewModel)
//Add Observer to articles to update Recyclerview
viewModel.articles.observe(viewLifecycleOwner, Observer {
swipeRefreshLayout.isRefreshing = false
})
//region Init SwipeRefresh Layout
swipeRefreshLayout = root.findViewById(R.id.thomsline_swipe_container)
swipeRefreshLayout.setOnRefreshListener(this)
swipeRefreshLayout.setColorSchemeResources(
R.color.primaryColor,
R.color.secondaryColor
)
if(viewModel.isEmpty()) {
swipeRefreshLayout.post {
// Display Refresh Indicator
swipeRefreshLayout.isRefreshing = true
// Load First Article Page
loadArticles(0, true)
}
}
//endregion
//region Init Recycler View
_binding?.thomslineRecyclerView?.apply {
layoutManager = LinearLayoutManager(this#ThomsLineFragment.context)
addItemDecoration(ArticleListSpacingDecoration())
adapter = recyclerAdapter
}
//endregion
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
/**
* Called when the SwipeRefresh Layout is triggerd
*/
override fun onRefresh() {
loadArticles(0, true)
}
/**
* Loads all Article pages until "page" and removes all cached pages after it
*/
fun loadArticles(page:Int, reloadAll: Boolean){
// Remove all cached pages after the given one
if(page == 0) {
viewModel.removeArticlePagesFromIndex(1, recyclerAdapter)
viewModel.lastPage = -1
}
// Create a new Request Queue
val requestQueue = Volley.newRequestQueue(this.context)
// Add requests to load the Pages to the requestQueue
if(reloadAll)
for (i in 0 until page+1) {
reloadPage(i, requestQueue)
}
else reloadPage(page)
}
// Reload a page without a given Request Queue
fun reloadPage(id:Int){
reloadPage(id, Volley.newRequestQueue(this.context))
}
// Reload a Page while adding the Requests to a given Request Queue
fun reloadPage(id: Int, requestQueue:RequestQueue) {
Log.d("ThomsLine", "Requesting Data for page $id")
// Start the Request
requestQueue.add(JsonArrayRequest(viewModel.BASE_URL + MainActivity.WORDPRESS_BASE_URL_LITE + "&&page=${id+1}",
{ response ->
Log.d("ThomsLine", "Got Data for page $id")
// A Variable to load the Articles to
val data = ArrayList<WordpressArticle>()
// Load the Articles from the JSON
for (j in 0 until response.length()) data.add(WordpressArticle(response.getJSONObject(j), true, viewModel.BASE_URL))
// Update the RecyclerView
viewModel.setArticlePage(id, WordpressPage(data.toTypedArray()), recyclerAdapter)
},
{ volleyError ->
Log.d("ThomsLine", "Request Error while loading Data for page $id")
// Check if the Error is caused because loading a non Existing Page
if (volleyError.networkResponse?.statusCode == 400){
// Update the Last Page Variable
viewModel.lastPage = if(id-1<viewModel.lastPage) viewModel.lastPage else id-1
recyclerAdapter.notifyItemChanged(recyclerAdapter.itemCount-1)
Log.d("ThomsLine", "Page does not exist (last page: ${viewModel.lastPage})")
} else {
Log.d("ThomsLine", "Request failed: ${volleyError.message.toString()}")
// Display a Snackbar, stating the Error
Snackbar.make(requireActivity().findViewById(R.id.app_bar_main), WordpressArticle.getVolleyError(volleyError, requireActivity()), Snackbar.LENGTH_LONG).show()
}
//recyclerAdapter.notifyItemChanged(id)
}
))
}
/**
* Called when a Article is clicked
*/
override fun onItemClick(wordpressArticle: WordpressArticle) {
val action = ThomsLineFragmentDirections.actionNavThomslineToNavThomslineArticleView(wordpressArticle.id)
findNavController().navigate(action)
}
}
You don't assign the ViewModel in your abstract class (which is what you're doing by using a delegate, by activityViewModels()). Just make it an abstract property in your abstract class, and then your concrete Fragment subclasses will be required to assign a value to it (using by activityViewModels() if you like):
abstract class BaseFragment : Fragment() {
...
abstract val viewModel: BaseViewModel
...
}
class RealFragment : BaseFragment() {
...
// you can specify a subtype of BaseViewModel in your implementation
override val viewModel: SomeViewModel by activityViewModels()
...
}

Can't share Viewmodel between Activity and Fragment:

Android Studio 3.6
Here my viewModel:
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
class BluetoothPageViewModel(application: Application) : AndroidViewModel(application) {
private val isSearchingTableModeLiveData = MutableLiveData<Boolean>()
private val isInitModeLiveData = MutableLiveData<Boolean>()
private val errorMessageLiveData = MutableLiveData<String>()
private val toastMessageLiveData = MutableLiveData<String>()
fun isInitModeLiveData(): LiveData<Boolean> {
return isInitModeLiveData
}
fun isSearchingTableModeLiveData(): LiveData<Boolean> {
return isSearchingTableModeLiveData
}
fun getErrorMessageLiveData(): LiveData<String> {
return errorMessageLiveData
}
fun getToastMessageLiveData(): LiveData<String> {
return toastMessageLiveData
}
Here fragment the subscribe to this viewmodel and success call Observer.onChanged()
class BluetoothPageFragment : Fragment() {
private lateinit var bluetoothPageViewModel: BluetoothPageViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
dataBinding =
DataBindingUtil.inflate(inflater, R.layout.bluetooth_page_fragment, container, false)
val view = dataBinding.getRoot()
dataBinding.setHandler(this)
init()
return view
}
private fun init() {
val context = this.context
val viewViewModelProvider = ViewModelProviders.of(this)
bluetoothPageViewModel = viewViewModelProvider.get(BluetoothPageViewModel::class.java)
bluetoothPageViewModel.isInitModeLiveData().observe(this, // SUCCESS CALL
Observer<Boolean> { isInitMode ->
})
}
Here my activity the subscribe to this viewmodel and NOT call Observer.onChanged()
import androidx.lifecycle.ViewModelProviders
class QRBluetoothSwipeActivity : AppCompatActivity() {
private lateinit var bluetoothPageViewModel: BluetoothPageViewModel
private fun init() {
val viewViewModelProvider = ViewModelProviders.of(this)
bluetoothPageViewModel = viewViewModelProvider.get(BluetoothPageViewModel::class.java)
val customFragmentStateAdapter = CustomFragmentStateAdapter(this)
customFragmentStateAdapter.addFragment(QrPageFragment())
bluetoothPageFragment = BluetoothPageFragment()
customFragmentStateAdapter.addFragment(bluetoothPageFragment)
dataBinding.viewPager2.adapter = customFragmentStateAdapter
initLogic()
}
private fun initLogic() {
dataBinding.viewPager2.registerOnPageChangeCallback(object :
ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
positionObservable.set(position)
}
})
bluetoothPageViewModel.getToastMessageLiveData() // this not call
.observe(this,
Observer<String> { message ->
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
})
}
Why not call getToastMessageLiveData() ?
In both cases you are using
ViewModelProviders.of(this)
It implifies you want this viewmodel with different scopes. One from Activity Scope and one from Fragment scope. If you want to share it.
If you want to share viewmodel you have to use single scope. I recommend using scope of bigger element, in this case activity.
In fragment you should call
ViewModelProviders.of(activity)
This should fix your issue.
you call ViewModelProviders.of(this) in both activity and fragment, but that is different contexts. So in your case, you instantiate 2 different instances of BluetoothPageViewModel, therefore onChanged callback is not called.
In order to share one instance between activity and a Fragment you should obtain viewModelProvider from the same context.
In your activity:
ViewModelProviders.of(this)
In your fragment:
ViewModelProviders.of(activity) or
activity?.let {
val bluetoothPageViewModel = ViewModelProviders.of(it).get(BluetoothPageViewModel::class.java)
bluetoothPageViewModel.isInitModeLiveData().observe(this, // SUCCESS CALL
Observer<Boolean> { isInitMode ->
})
}

How to properly implement an interface in android fragment?

folks. I have been working on a project with kotlin and I need to make a fragment that comunicate with the parent activity... I followed exactly what google and other websites suggested but I still get an error "activity does not override anything"... All of the other solutions are not working for me... here is the code .
FRAGMENT
package com.me.assistan.assistant
import android.app.Activity
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Context
import android.content.Intent
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.LinearLayout
import android.widget.ToggleButton
import kotlinx.android.synthetic.main.content_newplan.*
import java.util.*
class addingTask : Fragment(), View.OnClickListener{
var func = Functions
var globalTask = GlobalTask
private lateinit var listener: OnTimeSettedListener
override fun onAttach(context: Context?) {
super.onAttach(context)
if (context is OnTimeSettedListener) {
listener = context
} else {
throw ClassCastException(context!!.toString() + " must implement
OnTimeSettedListener.")
}
}
companion object {
fun newInstance(): addingTask {
return addingTask()
}
}
override fun onCreateView(inflater: LayoutInflater?, container:
ViewGroup?,
savedInstanceState: Bundle?): View? {
val view: View = inflater!!.inflate(R.layout.fragment_adding_task,
container,
false)
val activity = activity
view.theTime.setOnClickListener { v ->
listener.onTimeSetListtedener("test")
}
return view
}
interface OnTimeSettedListener{
fun onTimeSetListtedener(comic : String){
println("ok")
}
}
}// Required empty public constructor
And not the MAIN ACTIVITY
class Newplan : AppCompatActivity(), addingTask.OnTimeSettedListener {
var posx = 0f
private var startx = 0f
private var posy = 0f
private var starty = 0f
var backIntent = Intent();
var func = Functions
var globalTask = GlobalTask
val fragment = addingTask()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_newplan)
if(savedInstanceState === null){
var args = Bundle()
supportFragmentManager.beginTransaction()
.add(R.id.taskMain, addingTask.newInstance(),"newTask")
.commit()
}
}
override fun onTimeSettedListener(comic : String){
println("params")
}
}
I get the error on the activity class... When I remove the "override?, the error is gone but nothing happen when I click on the button... What am I doing wrong?
I think you shouldn't add method body to your interface method. It is not allowed in Java. In Kotlin there is no error that showing method body is restricted. But you should remove body. Change your interface like
interface OnTimeSettedListener{
fun onTimeSetListtedener(comic : String)
}
Also actually you are not overriding your Listener's method. Method name in OnTimeSettedListener is onTimeSetListtedener but you are overriding onTimeSettedListener which is not really exist in your code.
Also as #albodelu mentioned answer and #chris mentioned in comments, you cannot write methods in methods. It is not correct usage.
As #chris commented, you need to move the lines below outside of onCreate() method:
override fun onTimeSettedListener(comic: String) {
println("params")
}
You also need to match names replacing
interface OnTimeSettedListener {
fun onTimeSetListtedener(comic : String){
println("ok")
}
}
by
interface OnTimeSettedListener {
fun onTimeSettedListener(comic: String) {
println("ok")
}
}
Update
If you fix the name typo and remove the default implementation of the onTimeSettedListener declaration in the interface inside your fragment, as your activity implements it and Android Studio warns you about the missing override, it's possible to click it and select that the IDE implements it for you to avoid errors doing it:
interface OnTimeSettedListener{
fun onTimeSettedListener(comic : String)
}
You also need to fix the call replacing:
listener.onTimeSetListtedener("test")
by
listener.onTimeSettedListener("test")

Categories

Resources