I ran into a problem, I put some buttons in my fragment and in the code file wanted to use them but I had a problem. Do you know some methods of how can I solve it? I use Kotlin
I tried a lot of variants on how to use setOnClickListener in fragments but it didn't work for me. My code looks like this:
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
class WorkoutFragment : Fragment(),WorkoutAdapter.OnItemClickListener, WorkoutFragmentView {
private lateinit var presenter: WorkoutFragmentPresenter
private var param1: String? = null
private var param2: String? = null
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? {
val v = inflater.inflate(R.layout.fragment_workout, container, false)
return v
}
companion object {
#JvmStatic
fun newInstance(param1: String, param2: String) =
WorkoutFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
private fun InitRecyclerView(v:View) {
val recyclerView = v.findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = WorkoutAdapter(WorkoutProvider.WorkoutList, this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
InitRecyclerView(view)
presenter = WorkoutFragmentPresenter(this)
}}
I answered this yesterday here.
Passing the fragment's view into the adapter isn't the best solution IMHO, it's better to pass a click lambda as demonstrated in the above example. In any case, all you have to do after that is just set an onClickListener in the adapter's items.
Related
i have a really simple vocabulary note app contains 2 fragment and 1 root activity. In HomeFragment i have a button "addVocabularyButton". When it is clicked a BottomSheetDialogFragment appears and user gives 3 inputs and with a viewmodel it is saved in DB. My problem is when i save the input to the DB it works fine but i cannot see in HomeFragment that word instantaneously. I have to re-run the app to see in home fragment. I am using Navigation library and recycler view in home fragment.
Github link : https://github.com/ugursnr/MyVocabularyNotebook
Home Fragment
class HomeFragment : Fragment() {
private var _binding : FragmentHomeBinding? = null
private val binding get() = _binding!!
private var vocabularyAdapter = VocabulariesHomeAdapter()
private lateinit var sharedViewModel: AddVocabularySharedViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHomeBinding.inflate(layoutInflater,container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//sharedViewModel = ViewModelProvider(this)[AddVocabularySharedViewModel::class.java]
sharedViewModel = (activity as MainActivity).sharedViewModel
sharedViewModel.getAllVocabulariesFromDB()
observeAllVocabularies()
prepareRecyclerView()
addVocabularyOnClick()
vocabularyAdapter.onItemDeleteClicked = {
sharedViewModel.deleteVocabulary(it)
observeAllVocabularies()
}
}
private fun prepareRecyclerView(){
binding.recyclerViewHome.apply {
layoutManager = LinearLayoutManager(context)
adapter = vocabularyAdapter
}
}
private fun addVocabularyOnClick(){
binding.addVocabularyButton.setOnClickListener{
val action = HomeFragmentDirections.actionHomeFragmentToAddVocabularyBottomSheetFragment()
Navigation.findNavController(it).navigate(action)
}
}
private fun observeAllVocabularies(){
sharedViewModel.allVocabulariesLiveData.observe(viewLifecycleOwner, Observer {
vocabularyAdapter.updateVocabularyList(it)
})
}
}
Dialog Fragment
class AddVocabularyBottomSheetFragment : BottomSheetDialogFragment() {
private var _binding : FragmentAddVocabularyBottomSheetBinding? = null
private val binding get() = _binding!!
private lateinit var sharedViewModel: AddVocabularySharedViewModel
private var vocabularyInput : String? = null
private var translationInput : String? = null
private var sampleSentenceInput : String? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAddVocabularyBottomSheetBinding.inflate(layoutInflater,container,false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//sharedViewModel = ViewModelProvider(this)[AddVocabularySharedViewModel::class.java]
sharedViewModel = (activity as MainActivity).sharedViewModel
binding.addOrUpdateVocabularyButton.setOnClickListener {
vocabularyInput = binding.vocabularyActualET.text.toString()
translationInput = binding.vocabularyTranslationET.text.toString()
sampleSentenceInput = binding.vocabularySampleSentenceET.text.toString()
val inputVocabulary = Vocabulary(vocabularyInput,translationInput,sampleSentenceInput)
insertVocabularyToDB(inputVocabulary)
sharedViewModel.getAllVocabulariesFromDB()
dismiss()
}
}
private fun insertVocabularyToDB(vocabulary: Vocabulary){
sharedViewModel.insertVocabulary(vocabulary)
}
}
Shared ViewModel
class AddVocabularySharedViewModel(application: Application) : AndroidViewModel(application) {
private var _allVocabulariesLiveData = MutableLiveData<List<Vocabulary>>()
private var _vocabularyLiveData = MutableLiveData<Vocabulary>()
val allVocabulariesLiveData get() = _allVocabulariesLiveData
val vocabularyLiveData get() = _vocabularyLiveData
val dao = VocabularyDatabase.makeDatabase(application).vocabularyDao()
val repository = VocabularyRepository(dao)
fun insertVocabulary(vocabulary: Vocabulary) = CoroutineScope(Dispatchers.IO).launch {
repository.insertVocabulary(vocabulary)
}
fun updateVocabulary(vocabulary: Vocabulary) = CoroutineScope(Dispatchers.IO).launch {
repository.updateVocabulary(vocabulary)
}
fun deleteVocabulary(vocabulary: Vocabulary) = CoroutineScope(Dispatchers.IO).launch {
repository.deleteVocabulary(vocabulary)
}
fun getAllVocabulariesFromDB() = CoroutineScope(Dispatchers.IO).launch {
val temp = repository.getAllVocabulariesFromDB()
withContext(Dispatchers.Main){
_allVocabulariesLiveData.value = temp
}
}
fun getVocabularyDetailsByID(vocabularyID : Int) = CoroutineScope(Dispatchers.IO).launch {
val temp = repository.getVocabularyDetailsByID(vocabularyID).first()
withContext(Dispatchers.Main){
_vocabularyLiveData.value = temp
}
}
}
Adapter
class VocabulariesHomeAdapter : RecyclerView.Adapter<VocabulariesHomeAdapter.VocabulariesHomeViewHolder>() {
lateinit var onItemDeleteClicked : ((Vocabulary) -> Unit)
val allVocabulariesList = arrayListOf<Vocabulary>()
class VocabulariesHomeViewHolder(val binding : RecyclerRowBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VocabulariesHomeViewHolder {
return VocabulariesHomeViewHolder(RecyclerRowBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: VocabulariesHomeViewHolder, position: Int) {
val vocabulary = allVocabulariesList[position]
holder.binding.apply {
actualWordTV.text = vocabulary.vocabulary
translationWordTV.text = vocabulary.vocabularyTranslation
deleteButtonRV.setOnClickListener {
onItemDeleteClicked.invoke(vocabulary)
notifyItemRemoved(position)
}
}
}
override fun getItemCount(): Int {
return allVocabulariesList.size
}
fun updateVocabularyList(newList : List<Vocabulary>){
allVocabulariesList.clear()
allVocabulariesList.addAll(newList)
notifyDataSetChanged()
}
}
I know there are lots of codes up there but i have a really big problems about using these dialog fragments. Thank you for your help.
This is because multiple instances of the same View Model are created by the Navigation Library for each Navigation Screen.
You need to tell the Navigation Library to share the same ViewModel between all navigation screens.
Easiest way to fix this is to scope the viewModel to the Activity rather than a Fragment and using it in all your fragments.
val viewModel = ViewModelProvider(requireActivity()).get(MyViewModel::class.java)
This way, the viewModel is scoped to the Application instance rather than Fragment. This will keep the state in the viewModel persistent across the Application.
You can also do this by scoping the viewModel to the navigation graph.
val myViewModel: MyViewModel by navGraphViewModels(R.id.your_nested_nav_id)
Alternate method, if you're using dependency injection libraries
val navController = findNavController();
val navBackStackEntry = navController.currentBackStackEntry!!
If you use hilt, you can just pass your NavBackStackEntry of the NavGraph to hiltViewModel()
val viewModel = hiltViewModel<MyViewModel>(//pass NavBackStackEntry)
This will give you a viewModel that is scoped to NavBackStackEntry and will only be recreated when you pop the NavBackStackEntry(ie Navigate out of the navigation screens.)
I have two fragments that share information with each other, in the first one I have an edit text and button widget. The second fragment is just a listview. When the user clicks the button, it displays whatever is in the edit text widget in the second fragment.
So if the user enters the text study and clicks the button the second fragment will display
Study
If the user then enters the text eat and clicks the button, the second fragment will display
Study
Eat
I am having so issues with displaying the texts
So far this is what I have done
class FirstFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
viewModel = activity?.run { ViewModelProvider(this)[MyViewModel::class.java]
} ?: throw Exception("Invalid Activity")
val view = inflater.inflate(R.layout.one_fragment, container, false)
val button = view.findViewById<Button>(R.id.vbutton)
val value = view.findViewById<EditText>(R.id.textView)
button.setOnClickListener {
}
return view;
}
}
class SecondFragment : Fragment() {
lateinit var viewModel: MyViewModel
#SuppressLint("MissingInflatedId")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
viewModel = activity?.run { ViewModelProvider(this)[MyViewModel::class.java]
} ?: throw Exception("Invalid Activity")
val view = inflater.inflate(R.layout.page3_fragment, container, false)
val valueView = v.findViewById<TextView>(R.id.textView)
return view
The problem I am having is how to display the texts
If I undestand you correctly, you want to share data between fragments? If yes, you can do that with "shared" viewModel. For example:
class FirstFragment : Fragment() {
private var _binding: FragmentFirstBinding? = null
private val binding get() = _binding!!
private val sharedViewModel by activityViewModels<SharedViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentFirstBinding.inflate(inflater, container, false)
binding.buttonChangeFragment.setOnClickListener {
/*
You can change data here, or in navigateWithNavController() from
activity (You already have an instance of your viewModel in activity)
*/
sharedViewModel.changeData(binding.myEditText.text.toString())
if (requireActivity() is YourActivity)
(requireActivity() as YourActivity).navigateWithNavController()
}
return binding.root
}
}
class SecondFragment : Fragment() {
private var _binding: FragmentSecondBinding? = null
private val binding get() = _binding!!
private val sharedViewModel by activityViewModels<SharedViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSecondBinding.inflate(inflater, container, false)
binding.secondFragmentText.text = sharedViewModel.someData.value
return binding.root
}
}
and your activity:
class YourActivity: AppCompatActivity() {
private lateinit var binding: YourActivityBinding
private lateinit var appBarConfiguration: AppBarConfiguration
private val sharedViewModel: SharedViewModel by lazy {
ViewModelProvider(
this
)[SharedViewModel::class.java]
}
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = YourActivityBinding.inflate(LayoutInflater.from(this))
setContentView(binding.root)
navController = this.findNavController(R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration(navController.graph)
}
/*
This function is just for test
*/
fun navigateWithNavController() {
navController.navigate(R.id.secondFragment)
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, appBarConfiguration)
}
}
And your viewModel should look something like this:
class SharedViewModel : ViewModel() {
private val _someData = MutableLiveData("")
val someData: LiveData<String>
get() = _someData
fun changeData(newData: String?) {
_someData.value = newData ?: _someData.value
}
}
Your view model should have a backing list of the entered words. When a word is added, the list can be updated, and in turn you can update a LiveData that publishes the latest version of the list.
class MyViewModel: ViewModel() {
private val backingEntryList = mutableListOf<String>()
private val _entryListLiveData = MutableLiveData("")
val entryListLiveData : LiveData<String> get() = _entryListLiveData
fun addEntry(word: String) {
backingEntryList += word
_entryListLiveData.value = backingEntryList.toList() // use toList() to to get a safe copy
}
}
Your way of creating the shared view model is the hard way. The easy way is by using by activityViewModels().
I also suggest using the Fragment constructor that takes a layout argument, and then setting things up in onViewCreated instead of onCreateView. It's less boilerplate code to accomplish the same thing.
In the first fragment, you can add words when the button's clicked:
class FirstFragment : Fragment(R.layout.one_fragment) {
private val viewModel by activityViewModels<MyViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val button = view.findViewById<Button>(R.id.vbutton)
val value = view.findViewById<EditText>(R.id.textView)
button.setOnClickListener {
viewModel.addEntry(value.text.toString())
}
}
}
In the second fragment, you observe the live data:
class SecondFragment : Fragment(R.layout.page3_fragment) {
private val viewModel by activityViewModels<MyViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val valueView = view.findViewById<TextView>(R.id.textView)
viewModel.entryListLiveData.observe(viewLifecycleOwner) { entryList ->
valueView.text = entryList.joinToString(" ")
}
}
}
my fragment class
class RecyclerFragment : Fragment() {
var recycler:RecyclerView? =null
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
recycler = view?.findViewById(R.id.recycler)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_recycler, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
recycler?.layoutManager = LinearLayoutManager(this.context,RecyclerView.VERTICAL,false)
recycler?.adapter = Custom_Adapter() // error-Unresolved reference: Custom_Adapter
}
}
//here is my adater
class Custom_Adapter() : RecyclerView.Adapter<Custom_Adapter.ViewHolder>() {
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): Custom_Adapter.ViewHolder {
var myViewInflater = LayoutInflater.from(viewGroup.context).inflate(R.layout.custom_layout,viewGroup,false)
return ViewHolder(myViewInflater)
}
override fun onBindViewHolder(holder: Custom_Adapter.ViewHolder, position: Int) {
}
override fun getItemCount(): Int {
return 9
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
}
Update your recycler fragment like this.
class RecyclerFragment : Fragment(R.layout.fragment_recycler) {
var recycler:RecyclerView? =null
private var param1: String? = null
private var param2: String? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recycler = view.findViewById(R.id.recycler)
recycler?.layoutManager = LinearLayoutManager(this.context,RecyclerView.VERTICAL,false)
recycler?.adapter = Custom_Adapter()
}
}
And you have to move classes Recucler_Item and Custom_Adapter into correct java package.
From your repo link in one of the comments, Custom_Adapter is under your src/test directory. It needs to be under src/main where RecyclerFragment can see it - that's why you're getting unresolved reference errors. Stick it in there with everything else
main and test are different source sets, it's basically a way of keeping stuff separate. All your production code goes in main, and it can't see anything in test by default. When you run tests, those use everything in test (and maybe androidTest) and get all the stuff in main included, since the tests need access to the production code to, y'know, test it!
Project1
I have created a app that scans nearby wifidirect enabled devices whose UI was simple and had only one layout(activitymain.xml) and the code was in MainActivity.java & WifiDirectBroadcastReceiver. (Code can be found here: Can't find nearby WiFi- Direct devices showing "No Device Found!")
Project2
Now, I want to use Tablayout(custom not from default) which contains 2 tabs so I have to use 2 fragments.
Where should I place the code that was in MainActivity(project1)?
should I copy to fragment1 or MainActivity(Project2)
You have to copy the code of MainActivity (project1) to fragment of the tabbed layout. And then configure the SectionPagerAdapter like below.
Also you have to change some code of your MainActivity so that it gets fitted into the fragment.
private val TAB_TITLES = arrayOf(
R.string.tab_text_1,
R.string.tab_text_2
)
/**
* A [FragmentPagerAdapter] that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
class SectionsPagerAdapter(private val context: Context, fm: FragmentManager)
: FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment {
var fragment: Fragment? = null
when (position) {
0 -> fragment = Fragment1("f1","f1")
1 -> fragment = Fragment2("f2","f2")
}
return fragment!!
}
override fun getPageTitle(position: Int): CharSequence? {
return context.resources.getString(TAB_TITLES[position])
}
override fun getCount(): Int {
// Show 2 total pages.
return 2
}
}
You can create fragments like this:
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
class Fragment1 : Fragment() {
private var param1: String? = null
private var param2: String? = null
private var _binding: Fragment1Binding? = null
private val binding get() = _binding!!
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 {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onPause() {
super.onPause()
}
override fun onResume() {
super.onResume()
}
companion object {
#JvmStatic
fun newInstance(param1: String, param2: String) =
HomeFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
You can make Fragment2 like this and attach this to your tabbed layout.
The situation is pretty straightforward. I have a simple android app with 4 fragments displayed through a bottom navigation bar, and a central Room database. Each fragment should be able to perform CRUD operations on the DB through a viewmodel (details are probably irrelevant but I'll show this as well to be sure):
class ViewModel(application: Application): AndroidViewModel(application) {
val readAllIngredients: LiveData<List<Ingredient>>
val readAllRecipes: LiveData<List<Recipe>>
private val ingredientRepository: IngredientRepository
private val recipeRepository: RecipeRepository
init {
val ingredientDAO = ShoppingAppDatabase.getDatabase(application).ingredientDAO()
val recipeDAO = ShoppingAppDatabase.getDatabase(application).recipeDAO()
ingredientRepository = IngredientRepository(ingredientDAO)
recipeRepository = RecipeRepository(recipeDAO)
readAllIngredients = ingredientRepository.allIngredients
readAllRecipes = recipeRepository.allRecipes
}
fun addIngredient(ingredient: Ingredient) {
viewModelScope.launch(Dispatchers.IO) {
ingredientRepository.put(ingredient)
}
}
fun deleteIngredient(ingredient: Ingredient) {
viewModelScope.launch(Dispatchers.IO) {
ingredientRepository.delete(ingredient)
}
}
fun addRecipe(recipe: Recipe) {
viewModelScope.launch(Dispatchers.IO) {
recipeRepository.put(recipe)
}
}
fun updateRecipe(recipe: Recipe) {
viewModelScope.launch(Dispatchers.IO) {
recipeRepository.update(recipe)
}
}
fun updateIngredient(ingredient: Ingredient) {
viewModelScope.launch(Dispatchers.IO) {
ingredientRepository.update(ingredient)
}
}
}
I'm initializing a viewmodel in each fragment, but with limited success. Here's a fragment for which everything works fine:
class InventoryFragment() : Fragment() {
private var listAdapter = IngredientAdapter()
private lateinit var viewModel : ViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_inventory, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listAdapter = IngredientAdapter()
recycler_view.apply {
layoutManager = LinearLayoutManager(activity)
adapter = listAdapter
}
viewModel = ViewModelProvider(this).get(ViewModel::class.java)
viewModel.readAllIngredients.observe(viewLifecycleOwner, Observer { ingredient -> listAdapter.setData(ingredient) })
add_button.setOnClickListener{
val errorMessages = validateInput()
if(errorMessages.isNotEmpty()) {
displayToast(activity, errorMessages)
}
else {
viewModel.addIngredient(Ingredient(
edit_name.text.toString(),
edit_qty.text.toString().toFloat(),
edit_um.text.toString()
))
listAdapter.notifyDataSetChanged()
displayToast(activity, "Ingredient added")
hideKeyboard(activity, requireView().windowToken)
}
clearInput()
}
}
}
I'm initializing it in the onViewCreated callback and yeah, it works fine. Doing the same thing in a different fragment yields.. different results for some reason.
class BrowseFragment() : Fragment() {
private lateinit var viewModel: ViewModel
var recipeAdapter = RecipeAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_browse, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(ViewModel::class.java)
viewModel.readAllRecipes.observe(viewLifecycleOwner, Observer { recipe -> recipeAdapter.setData(recipe) })
submit_button.setOnClickListener{
var submitFragment = SubmitFragment(recipeAdapter)
var tr = (view.context as FragmentActivity).supportFragmentManager.beginTransaction()
tr.replace(R.id.fragment_container, submitFragment)
tr.commit()
}
browse_recycler_view.apply {
layoutManager = LinearLayoutManager(activity)
adapter = recipeAdapter
}
}
}
When I try to initialize the viewmodel in onViewCreated, I get an IllegalStateException: Can't access ViewModels from detached fragment exception. Creating it in onCreate doesn't work either, since the lifecycle owner is null, which makes sense I guess. What exactly am I doing wrong here?