override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_list, container, false)
val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(requireActivity())
noDataTextView = view.findViewById(R.id.no_data_textView)
noDataImageView = view.findViewById(R.id.no_data_imageView)
mToDoViewModel.getAllData.observe(viewLifecycleOwner, Observer { data ->
adapter.setData(data)
mSharedViewModel.checkIfDatabaseEmpty(data)
})
floatingActionButton = view.findViewById<FloatingActionButton>(R.id.floatingActionButton)
listLayout = view.findViewById(R.id.listLayout)
floatingActionButton.setOnClickListener {
findNavController().navigate(R.id.action_listFragment_to_addFragment)
}
//set menu
setHasOptionsMenu(true)
mSharedViewModel.emptyDatabase.observe(viewLifecycleOwner, Observer { data ->
showEmptyDatabaseViews(data)
})
return view
}
I have a visibility system going on where if the database is empty then the image is shown.
but when I run the code first image shows up then the data shows up then I debugged it and seen that mSharedViewModel.emptyDatabase.observe() function is running first? what is the main issue here,
ps, I am using suspended fun to load the data
Edit 1:
my default visibility is invisible
<ImageView>
.
.
android:visibility="invisible"
this is my ShareViewModel Class Which will check the database empty or not
class SharedViewModel(application: Application) : AndroidViewModel(application) {
val emptyDatabase: MutableLiveData<Boolean> = MutableLiveData(true)
fun checkIfDatabaseEmpty(toDoData: List<ToDoData>){
emptyDatabase.value=toDoData.isEmpty()
}
and this my ViewModel
class ToDoViewModel(application: Application):AndroidViewModel(application) {
private val toDoDao= ToDoDatabase.getDatabase(application).ToDoDao()
private val repository:ToDoRepository
val getAllData: LiveData<List<ToDoData>>
init {
repository=ToDoRepository(toDoDao)
getAllData=repository.getAllData
}
Your expectation:
I have a visibility system going on where if the database is empty then the image is shown.
According to your code:
android:visibility="invisible"
The default visibility is invisible okay but check the view model code
val emptyDatabase: MutableLiveData<Boolean> = MutableLiveData(true)
You set the value to true. So when any observer start observing the changes, the default value will be passed to the observer, so logically your code is OK, database is empty and image view is visible.
So, you should set false as the default value.
Related
I have an app which creates "tasks" and creates a countdown for each one.
I have a view model with a list, and it's observable via LiveData
val tasksList = mutableListOf<Task>()
private val _tasksListData = MutableLiveData(tasksList)
val tasksListData : LiveData<MutableList<Task>>
get() = _tasksListData
fun addNewTask(task : Task){
tasksList.add(task)
_tasksListData.value = tasksList
}
I have already check that the items are created via a log statement. So that's working alright.
Then in the fragment I'm observing this live data and trying to add dynamically each task.
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_history,
container,
false
)
taskListContainer = binding.tasksListContainer
tasksViewModel = ViewModelProvider(requireActivity()).get(TasksViewModel::class.java)
//Checks if the list has some items, otherwise displays a message
checkTasksList()
tasksViewModel.tasksListData.observe(viewLifecycleOwner,{
for (item in it) {
val view: IndividualTaskViewBinding = DataBindingUtil.inflate(
inflater, R.layout.individual_task_view, container, false
)
view.taskTitle.text = item.name
view.taskDateCreated.text = item.dateCreated
view.taskTertiaryText.text = item.cyclesCompleted.toString()
taskListContainer.addView(view.root)
}
checkTasksList()
})
setHasOptionsMenu(true)
return binding.root
}
private fun checkTasksList(){
if(taskListContainer.childCount == 0 ){
binding.emptyListText.setVisibility(View.VISIBLE)
} else{
binding.emptyListText.setVisibility(View.GONE)
}
}
}
The problem is that onCreateView() is called just once, and then I can't find a way to Observe the LiveData again. I have checked with Log statements, but while I'm swapping tabs (meaning I'm swapping fragments), I can't get any method from the Fragment's lifecycle.
The entire project is here: https://github.com/arieldipietro/PomodoroTechnique
I am switching the screen using the Navigation Component.
In the fragment screen of the bottom A menu, i can add a recyclerview item dynamically through the button.
If i press the button on this screen, it moves to another fragment where data can be selected.
If i select data on the converted fragment screen, it returns to the previous screen and adds a recycler view item based on the selected data.
This process is repeated.
However, even if I repeat this process, the item is not dynamically added.
I add items to the List of LiveData by using the ViewModel, but as a result of debugging, the size of the list type of LiveData does not increase from only one.
At least as far as I know the data should be persisted because using viewmodel is not affected by lifecycle.
But the problem I have is that it seems to be initialized and saved every time because of the screen change.
Why is this?
ViewModel
class WriteRoutineViewModel : ViewModel() {
private var _items: MutableLiveData<ArrayList<RoutineModel>> = MutableLiveData(arrayListOf())
val items: LiveData<ArrayList<RoutineModel>> = _items
fun addRoutine(workout: String) {
val item = RoutineModel(workout, "TEST")
item.setSubItemList(detailItem)
_items.value?.add(item)
_items.value = _items.value
}
}
Fragment
class WriteRoutineFragment : Fragment() {
private var _binding : FragmentWriteRoutineBinding? = null
private val binding get() = _binding!!
private lateinit var adapter : RoutineAdapter
private val args : WriteRoutineFragmentArgs by navArgs()
private val vm : WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = FragmentWriteRoutineBinding.inflate(inflater, container, false)
adapter = RoutineAdapter(::addDetail, ::deleteDetail)
binding.rv.adapter = this.adapter
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
args.workout?.let { workout ->
vm.addRoutine(workout)
Toast.makeText(context, workout, Toast.LENGTH_SHORT).show()
}
vm.items.observe(viewLifecycleOwner) { updatedItems ->
adapter.setItems(updatedItems)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
UPDATE nav_graph
<fragment
android:id="#+id/writeRoutine"
android:name="com.example.lightweight.fragment.WriteRoutineFragment"
android:label="fragment_write_routine"
tools:layout="#layout/fragment_write_routine" >
<action
android:id="#+id/action_writeRoutineFragment_to_workoutListTabFragment"
app:destination="#id/workoutListTabFragment" />
<argument
android:name="workout"
app:argType="string"
app:nullable="true"
android:defaultValue="#null"/>
</fragment>
The view model should have an activity scope for the view model to be able to live throughout the activity lifecycle.
The view model must be initialized like this,
private val model: SharedViewModel by activityViewModels()
This exact use-case is explained in detail in the Android Docs
My recyclerview doesn't udpate with the livedata when I remove an
item.
The function jokeisclicked() opens up a dialog for the user which
can choose to edit or delete an item of the room database.
The delete completes, but only when I refresh the tab. How can I complete the delete without refreshing the tab?
class DashboardFragment : Fragment() {
private lateinit var dashboardViewModel: DashboardViewModel
lateinit var binding: FragmentDashboardBinding
lateinit var adapter: JokeRecyclerViewAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//init repo and viewmodel
val dao: JokeDao = RoomDB.getInstance(requireContext()).JokeDAO
val jokerepo = JokeRepo(dao, RetrofitBuilder.jokeservice)
val factory = DashboardViewModelFactory(jokerepo)
dashboardViewModel = ViewModelProvider(this, factory).get(DashboardViewModel::class.java)
//init binding
binding = FragmentDashboardBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
//recycler observe from livedata
dashboardViewModel.jokes.observe(viewLifecycleOwner, {
binding.recyclerview.layoutManager = LinearLayoutManager(this.requireContext())
adapter = JokeRecyclerViewAdapter(it, { selected: Joke -> jokeIsClicked(selected) })
binding.recyclerview.adapter = adapter
})
return binding.root
}
fun jokeIsClicked(joke: Joke) {
//show pop up dialog
val dialog = PopUpFragment(joke)
getFragmentManager()?.let { dialog.show(it, "popUpDialog") }
}
}
here is the popupfragment class
class PopUpFragment(private val selectedJoke : Joke) : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
//init viewmodel
val dao = RoomDB.getInstance(this.requireContext()).JokeDAO
val jokerepo = JokeRepo(dao,RetrofitBuilder.jokeservice)
val factory = PopUpViewModelFactory(jokerepo)
val popupViewModel : PopUpFragmentViewModel = ViewModelProvider(this,factory).get(PopUpFragmentViewModel::class.java)
//init binding
val binding = FragmentPopUpBinding.inflate(inflater,container,false)
//edit clicked
binding.editjoke.setOnClickListener{
}
//delete clicked
binding.deletejoke.setOnClickListener {
//TODO
popupViewModel.deleteJoke(selectedJoke)
this.dismiss()
}
return binding.root
}
}
I think you dont update value of livedata at viewModel. Could you share viewmodles also ? If dialogViewModel removes joke from database than you have to return at #Dao's method LiveData or Flow.
Any way don't set adapert and layout manager at observe method - it's point less. For updating list at adaper you could create method and call in it notifiDataSetChanged() or (the best way of handling changes at adaper) diffUtils.
You'll need to do 2 things:
1 - Remove the data from your variable i.e if you're storying it within a an array then use array = ArrayUtils.remove(array, index);
2 - run .notifyDataSetChanged() on your RecyclerView's Adapter in order refresh the view based on the values of the new data set.
Using Kotlin, Retrofit and Coroutines, I have defined an interface to get data from a remote server and most importantly pass the id of a selected RecyclerView item back to the server.
interface CourseService {
#GET("/mobile/feed/course_data.php")
suspend fun getCourseData(#Query("pathName") pathName: String): Response<List<Course>>
}
Here, i get the id of the selected item from a RecyclerView from my MainFragment and store it in "selectedItem" variable.
override fun onPathItemClick(path: Path) {
viewModel.selectedItem.value = path
selectedItem= viewModel.selectedItem.value!!.path_id
navController.navigate(R.id.action_mainFragment_to_courseFragment)
}
I pass the value of selected item to the getCourseData() function
class CourseRepository(val app: Application) {
val courseData = MutableLiveData<List<Course>>()
init {
CoroutineScope(Dispatchers.IO).launch {
callWebService()
}
}
#WorkerThread
suspend fun callWebService() {
val retrofit = Retrofit.Builder().baseUrl(WEB_SERVICE_URL).addConverterFactory(MoshiConverterFactory.create()).build()
val service = retrofit.create(CourseService::class.java)
val serviceData = service.getCourseData(selectedItem).body() ?: emptyList()
courseData.postValue(serviceData)
}
}
But i get no results and it seems as though the value passed to getCourseData() function is null, but when checking the log is does have a value.
so if i give it a predefined value anywhere in my code like below, everything works completely fine
selectedItem= "MOB001"
val serviceData = service.getCourseData(selectedItem).body() ?: emptyList()
However, i cannot give it a fixed value prior to runtime because the value is retrieved when the user selects an item from a RecyclerView.
These are my multiple logs:
2020-05-01 13:56:30.431 23843-23843/ I/mylog: Main Fragment before item click: selectedItem =
2020-05-01 13:56:37.757 23843-23843/ I/mylog: Main Fragment after item click: selectedItem = WEB001
2020-05-01 13:56:37.763 23843-23843/ I/mylog: Course Fragment onCreateView(): selectedItem = WEB001
2020-05-01 13:56:37.772 23843-23901/ I/mylog: Course Fragment CourseRepository: selectedItem = WEB001
How can i overcome this issue?
You should call your CourseRepository's suspend function callWebService inside your ViewModel. Here is your repository:
class CourseRepository(val app: Application) {
suspend fun callWebService(path: Path): List<Course> {
return withContext(Dispatchers.IO) {
val retrofit = Retrofit.Builder().baseUrl(WEB_SERVICE_URL).addConverterFactory(MoshiConverterFactory.create()).build()
val service = retrofit.create(CourseService::class.java)
service.getCourseData(path.path_id).body() ?: emptyList()
}
}
}
Then you should call your repository function in your ViewModel as follows:
fun getCourseData(path: Path): LiveData<List<Course>> {
val response = MutableLiveData<List<Course>>()
viewModelScope.launch {
response.postValue(repository.callWebService(path))
}
return response
}
Then call viewModel. getCourseData(path) from your Activity or Fragment or anywhere when you get valid Path value.
Don't forget to include implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" to your gradle file.
Your code seems to be correct, however, it is highly possible that your RecyclerView is being populated the first time and and evertime you go back and choose another path it is being populated with the same data and view.
Therefore, your attentions should be focused on why the data is not being fetched again, which is the cause of the RecyclerView and Fragment holding on to the same first view.
After days of thinking my code was wrong, it turned out that my RecyclerView adapter was loading the same view everytime i wen back to select a different path becuase my RecyclerView was being inflated in the onCreateView() function which is only called once only, when a fragment is inflated the first time.
class CourseFragment : Fragment(),
CourseRecyclerAdapter.CourseItemListener {
private lateinit var viewModel: CourseViewModel
private lateinit var recyclerView: RecyclerView
private lateinit var navController: NavController
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_course, container, false)
recyclerView = view.findViewById(R.id.courseRecyclerView)
navController = Navigation.findNavController(requireActivity(), R.id.nav_host )
viewModel = ViewModelProvider(requireActivity()).get(CourseViewModel::class.java)
viewModel.courseData.observe(viewLifecycleOwner, Observer {
val adapter =
CourseRecyclerAdapter(
requireContext(),
it,
this
)
recyclerView.adapter = adapter
} )
return view
}
override fun onCourseItemClick(course: Course) {
viewModel.selectedCourse.value = course
navController.navigate(R.id.action_courseFragment_to_detailFragment)
}
}
I'm trying to learn how to implement databinding in an Android app. I have a small app I'm working with to learn this. And while I have databinding working for part of the app. I have hit a hiccup when trying to implement a recyclerview. I just cannot seem to get it. Been banging away at it for two or three days, and getting frustrated. Thought I'd ask you guys.
The app is super simple at this point.
The part i'm stuck on is accessing my recyclerview from an .xml layout from my MainFragment.kt
At first I was trying to use binding, but got frustrated and went back to just trying to use findViewById, but that is giving me issue too. I am beginning to think, I don't have as firm a grasp on databinding as I thought I did.
This is from the fragment that holds the recyclerView:
fragment_main.xml
<androidx.recyclerview.widget.RecyclerView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"
android:id="#+id/job_recyclerView"/>
I have another small layout file that is using Cardview to show each individual item in the recyclerview
A super simple Model:
JobData.kt
data class JobData(val companyName: String, val location: String)
An Adapter:
JobAdapter.kt
class CustomAdapter(val userList: ArrayList<JobData>) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
//Returning view for each item in the list
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomAdapter.ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.job_item_layout, parent, false)
return ViewHolder(v)
}
//Binding the data on the list
override fun onBindViewHolder(holder: CustomAdapter.ViewHolder, position: Int) {
holder.bindItems(userList[position])
}
override fun getItemCount(): Int {
return userList.size
}
//Class holds the job list view
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindItems(job: JobData) {
val textViewName = itemView.findViewById(R.id.tv_company_name) as TextView
val textViewAddress = itemView.findViewById(R.id.tv_Location) as TextView
textViewName.text = job.companyName
textViewAddress.text = job.location
}
}
}
And then the code in my MainFragment to handle it all, which it is not doing. I've tried everything, it was getting ugly. As you can see below. Binding is in place and working for my FloatingActionButton. But I for some reason cannot figure out how to access that recylerview. At the point the code is at below, I thought I'd just accessing using findViewById, but that is not working either.
MainFragment.kt
class MainFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding: FragmentMainBinding = DataBindingUtil.inflate(
inflater, R.layout.fragment_main, container, false)
//Setting onClickListener for FAB(floating action button) using Navigation
binding.createNewJobFAB.setOnClickListener { v: View ->
v.findNavController().navigate(R.id.action_mainFragment_to_createNewJobFragment)
}
//getting recyclerview from xml
val recyclerView = findViewById(R.id.job_recyclerView) as RecyclerView
//adding a layoutmanager
recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
//Arraylist to store jobs using the data class JobData
val jobs = ArrayList<JobData>()
//add dummy data to list
jobs.add(JobData("A Company", "Town A"))
jobs.add(JobData("B Company", "Town B"))
jobs.add(JobData("C Company", "Town C"))
jobs.add(JobData("D Company", "Town D"))
//creating adapter
val adapter = CustomAdapter(jobs)
//add adapter to recyclerView
recyclerView.adapter = adapter
return binding.root
}
}
The above fails to compile for two reasons:
findViewById shows as an "Unresolved Reference".
When adding the layoutManager, "this" shows as a "Type Mismatch"
Which I believe is due to the fact that Fragments do not have a context. Or so, I think anyway. But I don't know to resolve that? Maybe override some other method, but I can't seem to figure out which or how?
Oh and MainActivity looks like:
MainActivity.kt
class MainActivity : AppCompatActivity() {
//private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
#Suppress("UNUSED_VARIABLE")
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
}
//Ensures back button works as it should
override fun onSupportNavigateUp() = findNavController(this, R.id.navHostFragment).navigateUp()
}
Which is pointing to Nav_Graph for Android Navigation (part of JetPack). This bit is fine and working.
Adding gradle files to show that my dependencies were set correctly as suggested below.
app/gradle
android {
compileSdkVersion 28
dataBinding {
enabled = true
}
...
}
kapt {
generateStubs = true
correctErrorTypes = true
}
dependencies {
...
kapt "com.android.databinding:compiler:$gradle_version"
...
}
Encase your xml in <layout>..<layout/>
private lateinit var binding: FragmentXXXBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentXXXBinding.inflate(inflater)
return binding.root
}
Then you can call recyclerview by binding.jobRecyclerview
try to set all the click listeners etc on onViewCreated rather than onCreateView of fragment
It is wrong way to findViewById from Fragment(it is good technique for Activity):
val recyclerView = findViewById(R.id.job_recyclerView) as RecyclerView
First, fragment's layout have to be return by onCreateView() method.
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_main, container, false)
}
I personally like do all fragment's business logic inside onViewCreated()
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Now, we can use views by kotlinx
//val recyclerView = job_recyclerView
//Or old-fashioned way
val recyclerView = getView()!!.findViewById(R.id.job_recyclerView) as RecyclerView
}
RecylerView can be accessed from fragment's layout by having root view like: getView()!!.findViewById or by kotlinx inside onViewCreated(): job_recyclerView
Ok, so first of all you are getting error on findViewById because your fragment is unaware about the view that contains recyclerView
What you should do is, take an instance of view that you are inflating for this fragment (declare view as a global variable, replace your inflater line with this).
var rootView
// Inside onCreateView
var rootView = inflater?.inflate(R.layout.fragment, container, false)
Now replace, findViewById() with rootView.findViewById()
And the other error is because the fragment does not have any context of it's own so replace this with activity!!
By writing activity!! you are calling getActicity() method which returns context of parent activity.