I am now developing an application where I use MVVM pattern for UI and repository interaction. In other words, I receive live data object with a list of models from my ROOM data base query via repository, then assign it to my live data variable in viewmodel. After that, this data should be populated to my xml layout recycler view via data binding, but It happens only once fragment is initialised. In other cases recycler view is void
DAO code :
#Query("SELECT * FROM note WHERE content LIKE :query ORDER BY isStarred")
fun searchAllNotes(query : String?) : Flow<List<Note>>
ViewModel code:
#HiltViewModel
class HomeViewModel #Inject constructor (
private val noteRepository : NoteRepository
) : ViewModel() {
private var _notesLiveData : LiveData<List<Note>> = noteRepository.getAllNotes().asLiveData()
val notesLiveData get()= _notesLiveData
fun searchNotes(query : String){
viewModelScope.launch(Dispatchers.IO){
_notesLiveData = noteRepository.searchAllNotes(query).asLiveData()
}
}
fun deleteNote(note : Note){
viewModelScope.launch(Dispatchers.IO){
noteRepository.deleteNote(note)
}
}
fun updateNoteChecked(note : Note){
viewModelScope.launch(Dispatchers.IO){
noteRepository.updateNoteChecked(note.id, note.isStarred)
}
}
Fragment code
#AndroidEntryPoint
class HomeFragment : Fragment(),
NoteCardAdapter.NoteTouchListener,
SearchView.OnQueryTextListener{
private var _binding : FragmentHomeBinding? = null
val binding get() = _binding!!
private val adapter by lazy {
NoteCardAdapter(this as NoteCardAdapter.NoteTouchListener)
}
private val vm : HomeViewModel by viewModels()
private val noteSharedViewModel : NoteSharedViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHomeBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.adapter = adapter
binding.vm = vm
binding.apply {
lifecycleOwner = viewLifecycleOwner
homeRecyclerView.isNestedScrollingEnabled = false
homeRecyclerView.layoutManager = LinearLayoutManager(view.context)
homeRecyclerView.itemAnimator= NotesItemAnimator()
}
binding.homeSearchView.isSubmitButtonEnabled = true
binding.homeSearchView.setOnQueryTextListener(this as SearchView.OnQueryTextListener)
binding.addNoteButton.setOnClickListener{
val note = Note(
"","","",false, activity?.getDate(), folderId = -1
)
noteSharedViewModel.selectNote(note)
val action = HomeFragmentDirections.actionHomeFragmentToSingleNoteFragment(
isNew = true
)
findNavController().navigate(action)
}
binding.foldersButton.setOnClickListener{
findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToFoldersFragment())
}
}
xml layout code :
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
>
<data>
<variable
name="vm"
type="com.example.leonidsnotesapplication.presentation.notes_feature.viewmodels.HomeViewModel" />
<variable
name="adapter"
type="com.example.leonidsnotesapplication.presentation.notes_feature.adapters.NoteCardAdapter" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:background="#color/note_background_color_3"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".presentation.notes_feature.fragments.HomeFragment">
<androidx.appcompat.widget.SearchView
android:id="#+id/homeSearchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:theme="#style/AppSearchView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="#+id/tvRecentTitle"
android:textStyle="bold"
android:textSize="25sp"
android:textColor="#color/button_color_2"
android:text="#string/recent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="20dp"
app:layout_constraintTop_toBottomOf="#id/homeSearchView"
app:layout_constraintStart_toStartOf="parent"/>
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginTop="28dp"
android:background="#drawable/ic_circle_arrow"
app:layout_constraintStart_toEndOf="#id/tvRecentTitle"
app:layout_constraintTop_toBottomOf="#id/homeSearchView" />
<androidx.recyclerview.widget.RecyclerView
tools:listitem="#layout/note_card_view"
android:scrollbars="vertical"
android:scrollbarStyle="outsideInset"
android:id="#+id/homeRecyclerView"
android:layout_width="match_parent"
android:requiresFadingEdge="vertical"
android:fadingEdge="vertical"
android:fadingEdgeLength="15dp"
android:layout_height="500dp"
android:layout_marginTop="30dp"
app:setNoteAdapter="#{adapter}"
app:submitNoteList="#{vm.notesLiveData}"
app:layout_constraintTop_toBottomOf="#id/tvRecentTitle"
app:layout_constraintStart_toStartOf="parent"
/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:backgroundTint="#color/button_color_1"
android:id="#+id/add_note_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="#drawable/ic_create"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:backgroundTint="#color/button_color_1"
android:id="#+id/folders_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="#drawable/ic_folders_stack"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Data binding adapter :
#BindingAdapter("submitNoteList")
fun submitNoteList(recyclerView: RecyclerView, data : List<Note>?){
val adapter = recyclerView.adapter as NoteCardAdapter
adapter.setData((data as ArrayList<Note>? ?: arrayListOf()))
}
#BindingAdapter("setNoteAdapter")
fun setNoteAdapter(recyclerView: RecyclerView, adapter: NoteCardAdapter){
adapter.let {
recyclerView.adapter = it
}
}
You should observe your live data. Example code is:
vm.notesLiveData.observe(viewLifecycleOwner) { adapter.submitList(it) }
Related
I have a problem using viewmodel in fragment with data binding.
In my programm i'm using only one activity and fragments. I organized transitions between fragments using navigation. And i want to use data binding in my fragment, but when running the application only with binding, everything works well when I pass to the binding viewmodel when switching to this fragment of the application, it crashes.
I would be very grateful for your help!
My fragment
class TimeSettings : Fragment() {
private lateinit var viewModel: TimeSettingsViewModel
private lateinit var binding: FragmentTimeSettingsBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentTimeSettingsBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
binding.viewmodel = viewModel;
return binding.root
}
}
Object for viewmodel
object TimeData {
var inhale: Int = 4
private val _currentTimeInhale = MutableLiveData<Int>()
val currentTimeInhale: LiveData<Int>
get() = _currentTimeInhale
init{
_currentTimeInhale.value = inhale
}
fun changeCurrentTimeInhale(){
_currentTimeInhale.value = 5
}
}
Viewmodel
class TimeSettingsViewModel : ViewModel() {
val currentInhale: LiveData<Int>
get() = TimeData.currentTimeInhale
fun onChange() = TimeData.changeCurrentTimeInhale()
}
xml file for fragment
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="ivakin.first.my_first_app.viewmodels.TimeSettingsViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/textView"
android:layout_width="95dp"
android:layout_height="59dp"
android:text="#{viewmodel.currentInhale}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.452" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Set"
android:onClick="#{()->viewmodel.onChange()"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
U Forgot Initialize ViewModel :
viewmodel = TimeSettingsViewModel()
Also dont use return binding.root
I have simple registration form. When I enter data and change configuration the data is lost. I use ViewModel in my project and official documentation says ViewModel can handle orientation change automatically but it does not happen. How i suppose to store data with SaveState or I made a mistake in ViewModel?
Fragment code
class StartFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: StartFragmentBinding = DataBindingUtil.inflate(
inflater, R.layout.start_fragment, container, false)
val application = requireNotNull(this.activity).application
val dataSource = UsersDatabase.getInstance(application).usersDatabaseDao
val vm: SavedStateHandle by viewModels()
val viewModelFactory = StartFragmentViewModelFactory(dataSource, application)
val startFragmentViewModel =
ViewModelProvider(
this, viewModelFactory).get(StartFragmentViewModel::class.java)
binding.startFragmentViewModel = startFragmentViewModel
binding.lifecycleOwner = this
binding.start.setOnClickListener {
findNavController().navigate(
StartFragmentDirections
.actionStartFragmentToWebViewFragment())
startFragmentViewModel.doneNavigation()
}
return binding.root
}
}
ViewModel
class StartFragmentViewModel(
val database: UsersDatabaseDao,
application: Application
) : AndroidViewModel(application) {
private var viewModelJob = Job()
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private var user1 = MutableLiveData<User?>()
private val _navigateToWebView = MutableLiveData<User>()
val navigateToWebView: LiveData<User>
get() = _navigateToWebView
fun doneNavigation() {
_navigateToWebView.value = null
uiScope.launch {
val user = User()
insert(user)
}
}
private suspend fun insert(user: User) {
withContext(Dispatchers.IO) {
database.insert(user)
}
}
}
ViewModelFactory
class StartFragmentViewModelFactory (
private val dataSource: UsersDatabaseDao,
private val application: Application
) : ViewModelProvider.Factory {
#Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(StartFragmentViewModel::class.java)) {
return StartFragmentViewModel(dataSource, application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
start_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="startFragmentViewModel"
type="com.example.leadsdoittest.StartFragmentViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="#+id/margin_start"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="#+id/margin_end"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="#+id/margin_top"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.3" />
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/input_name"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="#id/margin_end"
app:layout_constraintStart_toStartOf="#id/margin_start"
app:layout_constraintTop_toTopOf="#id/margin_top">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/name" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/input_phone"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="#id/margin_end"
app:layout_constraintStart_toStartOf="#id/margin_start"
app:layout_constraintTop_toBottomOf="#id/input_name">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="phone"
android:hint="#string/phone_number" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/input_email"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="#id/margin_end"
app:layout_constraintStart_toStartOf="#id/margin_start"
app:layout_constraintTop_toBottomOf="#id/input_phone">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/email" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="#+id/start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:text="#string/start"
android:textColor="#android:color/white"
app:layout_constraintEnd_toEndOf="#id/margin_end"
app:layout_constraintStart_toStartOf="#id/margin_start"
app:layout_constraintTop_toBottomOf="#+id/input_email" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
View state is handled by the framework so you don't need to implement it yourself. However the saved state depends on having unique IDs on each view, so try adding an ID to each TextInputEditText.
Use instance variable to restore data after rotation. Look:
class StartFragment : Fragment() {
//private lateinit var homeViewModel: HomeViewModel
var name = ""
var email = ""
var phone = ""
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentHomeBinding.inflate(inflater)
binding.etName.doOnTextChanged { text, start, before, count ->
name = text.toString()
}
binding.etPhone.doOnTextChanged { text, start, before, count ->
phone = text.toString()
}
binding.etEmail.doOnTextChanged { text, start, before, count ->
email = text.toString()
}
binding.etName.setText(name)
binding.etEmail.setText(email)
binding.etPhone.setText(phone)
return binding.root
}
}
I write simple app displaying cocktails list. The problem is there's no title text but the pics is loading with picasso working just fine. I suppose the weak point in my Binding Adapter. Cause I can't just pass data value, i need to pass data?.list and I am really confused why I can't. Please help!
The app result
CocktailListFragment.kt
class CocktailListFragment : Fragment() {
private val viewModel: CocktailListViewModel by lazy {
ViewModelProvider(this).get(CocktailListViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = CocktailListFragmentBinding.inflate(inflater)
binding.lifecycleOwner = this
setHasOptionsMenu(true)
binding.cocktail = viewModel
binding.cocktailList.adapter = CocktailListAdapter()
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.filter_menu, menu)
super.onCreateOptionsMenu(menu, inflater)
}
}
CocktailListViewModel.kt
class CocktailListViewModel : ViewModel() {
private val _cocktails = MutableLiveData<CocktailsList>()
val cocktails: LiveData<CocktailsList>
get() = _cocktails
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(viewModelJob + Dispatchers.Main)
init {
getCocktails()
}
private fun getCocktails() {
coroutineScope.launch {
val getCocktailsDeferred = CocktailsApi.RETROFIT_SERVICE.getCocktailsAsync()
try {
val result = getCocktailsDeferred.await()
_cocktails.value = result
} catch (e: Exception) {
Log.d("error", "error")
}
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
CocktailListAdapter.kt
class CocktailListAdapter :
ListAdapter<Cocktail, CocktailListAdapter.CocktailListViewHolder>(DiffCallback) {
class CocktailListViewHolder(private var binding: CocktailItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(cocktail: Cocktail) {
binding.cocktail = cocktail
binding.executePendingBindings()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CocktailListViewHolder {
return CocktailListViewHolder(CocktailItemBinding.inflate(LayoutInflater.from(parent.context)))
}
override fun onBindViewHolder(holder: CocktailListViewHolder, position: Int) {
val cocktail = getItem(position)
holder.bind(cocktail)
}
companion object DiffCallback : DiffUtil.ItemCallback<Cocktail>() {
override fun areItemsTheSame(oldItem: Cocktail, newItem: Cocktail): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Cocktail, newItem: Cocktail): Boolean {
return oldItem.id == newItem.id
}
}
}
CocktailDataClass
data class CocktailsList(
#SerializedName("drinks")
val list: List<Cocktail>
)
data class Cocktail (
#SerializedName("idDrink")
val id: String,
#SerializedName("strDrinkThumb")
val drinkImg: String,
#SerializedName("strDrink")
val drinkTitle: String
)
BindingAdapters - I think the problem is in binding adapter for list data:
#BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, data: CocktailsList?) {
val adapter = recyclerView.adapter as CocktailListAdapter
adapter.submitList(data?.list)
}
#BindingAdapter("strDrinkThumb")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Picasso.get()
.load(imgUri)
.placeholder(R.drawable.loading_animation)
.error(R.drawable.ic_broken_image)
.into(imgView)
}
}
cocktail_list_fragemnt.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="cocktail"
type="com.example.coctaildb.cocktaillist.CocktailListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appBarLayout2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="30dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
style="#style/Widget.AppCompat.Toolbar"
android:background="#android:color/white"
app:menu="#menu/filter_menu"
app:title="#string/drinks"
app:titleTextColor="#android:color/black" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/cocktail_list"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/appBarLayout2"
app:listData="#{cocktail.cocktails}"
tools:listitem="#layout/cocktail_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
cocktail_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="cocktail"
type="com.example.coctaildb.network.Cocktail" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/cocktail_img"
android:layout_width="100dp"
android:layout_height="100dp"
android:maxHeight="100dp"
android:scaleType="centerCrop"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:strDrinkThumb="#{cocktail.drinkImg}"
tools:srcCompat="#tools:sample/avatars" />
<TextView
android:id="#+id/cocktail_title"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="21dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="20dp"
android:fontFamily="#font/roboto"
android:text="#{cocktail.drinkTitle}"
android:textColor="#7E7E7E"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="#+id/cocktail_img"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I am building an Android app using Kotlin.
I have a layout that contains a create_button. When the user clicks the button, the onClick should call onCreateJourney() in my ViewModel. This function requires the selectedPlaceId parameter to update a Journey entity with its value.
In my Fragment, I use the AutocompleteSupportFragment for the user to search and choose the desired place. This works well, as onPlaceSelected() returns the correct place in response to the user's selection. But I can't figure out how to use the returned place id value (p0.id) in my ViewModel onCreateJourney().
At this stage, the compiler throws this error:
cannot find method onCreateJourney() in class com.example.traveljournal.journey.NewJourneyViewModel
Please, can you shed some light on this issue for me?
My UI (.xml fragment layout):
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".journey.NewJourneyFragment">
<data>
<variable
name="newJourneyViewModel"
type="com.example.traveljournal.journey.NewJourneyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/whereQuestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="10dp"
android:text="#string/whereQuestion"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.027"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/journeyBckgImageView" />
<androidx.cardview.widget.CardView
android:id="#+id/cardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent">
</androidx.cardview.widget.CardView>
<fragment
android:id="#+id/autocomplete_fragment"
android:name="com.google.android.libraries.places.widget.AutocompleteSupportFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/whereQuestion" />
<Button
android:id="#+id/create_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:onClick="#{() -> newJourneyViewModel.onCreateJourney()}"
android:text="#string/createJourneyButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
My ViewModel
class NewJourneyViewModel (
private val journeyKey: Long = 0L,
val database: TravelDatabaseDao) : ViewModel() {
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private val _navigateToJourneys = MutableLiveData<Boolean?>()
val navigateToJourneys: LiveData<Boolean?>
get() = _navigateToJourneys
fun doneNavigating() {
_navigateToJourneys.value = null
}
fun onCreateJourney(selectedPlaceId: String) {
uiScope.launch {
withContext(Dispatchers.IO) {
val journey = database.getJourney(journeyKey) ?: return#withContext
journey.placeId = selectedPlaceId
database.updateJourney(journey)
}
_navigateToJourneys.value = true
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
My Fragment
class NewJourneyFragment : Fragment(), PlaceSelectionListener {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
(activity as AppCompatActivity).supportActionBar?.title = getString(R.string.createJourney)
val binding: FragmentNewJourneyBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_new_journey, container, false
)
val application = requireNotNull(this.activity).application
val arguments = NewJourneyFragmentArgs.fromBundle(arguments!!)
val dataSource = TravelDatabase.getInstance(application).travelDatabaseDao
val viewModelFactory = NewJourneyViewModelFactory(arguments.journeyKey, dataSource)
val newJourneyViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(NewJourneyViewModel::class.java)
binding.newJourneyViewModel = newJourneyViewModel
newJourneyViewModel.navigateToJourneys.observe(this, Observer {
if(it == true) {
this.findNavController().navigate(
NewJourneyFragmentDirections.actionNewJourneyDestinationToJourneysDestination())
newJourneyViewModel.doneNavigating()
}
})
if (!Places.isInitialized()) {
this.context?.let { Places.initialize(it, getString(R.string.apiKey), Locale.US) }
}
val autocompleteFragment = childFragmentManager.findFragmentById(R.id.autocomplete_fragment)
as? AutocompleteSupportFragment
autocompleteFragment?.setOnPlaceSelectedListener(this)
autocompleteFragment!!.setHint(getString(R.string.destinationExample))
autocompleteFragment!!.setPlaceFields(Arrays.asList(Place.Field.ID, Place.Field.NAME))
return binding.root
}
#ExperimentalStdlibApi
override fun onPlaceSelected(p0: Place) {
Log.i("PLACE", "Place: " + p0.name + ", " + p0.id)
}
override fun onError(status: Status) {
Toast.makeText(this.context,""+status.toString(),Toast.LENGTH_LONG).show()
Log.i("ERROR", "An error occurred: " + status)
}
}
You have to pass the field selectedPlaceId as an argument in the xml :
android:onClick="#{() -> newJourneyViewModel.onCreateJourney(newJourneyViewModel.selectedPlaceId)}"
How will you get the selectedPlaceId in view model depends on your programming logic.
For testing purposes, you can hardcode the value.
In NewJourneyViewModel, I set up selectedPlaceId as MutableLiveData.
val selectedPlaceId = MutableLiveData<String>()
In NewJourneyFragment, I used lateinit to create a field for NewJourneyViewModel called newJourneyViewModel and initialized it in onCreateView().
private lateinit var newJourneyViewModel : NewJourneyViewModel
Now having access to my ViewModel outside of onCreateView(), in my Fragment onPlaceSelected() method I can set the value of selectedPlaceId to p0.id.
newJourneyViewModel.selectedPlaceId.value = p0.id
If anybody can come up with a better and safer approach on this, it will be highly appreciated.
I have these activity, fragments, its viewmodels, and their adapter. I can already call the next fragment on click of a recyclerview item, but the new fragment overlays on the first fragment.
Refer to screenshot below:
Next screenshot is the old fragment view:
As for the mainactivity:
class MainActivity : AppCompatActivity(), RecyclerViewClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navView: BottomNavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.navigation_home,
R.id.navigation_messages,
R.id.navigation_notifications,
R.id.navigation_account
)
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.add(R.id.fragment_home, HomeFragment.newInstance(), "dormList")
.commit()
}
}
override fun onRecyclerViewItemClick(view: View, dorms: Dorms) {
val detailsFragment = dormDetailsFragment.newInstance(dorms)
supportFragmentManager
.beginTransaction()
.replace(R.id.fragment_home, detailsFragment, "Dorm Details")
.addToBackStack(null)
.commit()
}
}
HomeFragment:
class HomeFragment : Fragment(), RecyclerViewClickListener {
private lateinit var factory: HomeViewModelFactory
private lateinit var viewModel: HomeViewModel
private var callback : RecyclerViewClickListener? = null
companion object {
fun newInstance(): HomeFragment {
return HomeFragment()
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
if(context is RecyclerViewClickListener) callback = context
else throw ClassCastException("$context must implement Callback")
}
override fun onDetach() {
super.onDetach()
callback = null
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val api = DormsAPI()
val repository = DormRepository(api)
factory = HomeViewModelFactory(repository)
viewModel = ViewModelProviders.of(this, factory).get(HomeViewModel::class.java)
viewModel.getDorms()
viewModel.dorms.observe(viewLifecycleOwner, Observer { dorms ->
recyclerViewDorms.also{
it.layoutManager = LinearLayoutManager(requireContext())
it.setHasFixedSize(true)
it.adapter = dormAdapter(dorms, this)
}
})
}
override fun onRecyclerViewItemClick(view: View, dorms: Dorms) {
when(view.id){
R.id.button_reserve -> {
// TODO: Go to new account if not signed up, etc...
Toast.makeText(requireContext(), "Reserve button clicked", Toast.LENGTH_LONG).show()
}
R.id.layoutBox -> {
// TODO: Go to Dorm Details
callback?.onRecyclerViewItemClick(view, dorms)
}
}
}
}
Home View Model
class HomeViewModel(private val repository: DormRepository) : ViewModel() {
private lateinit var job: Job
private val _dorms = MutableLiveData<List<Dorms>>()
val dorms: LiveData<List<Dorms>>
get() = _dorms
fun getDorms() {
job = Coroutines.ioThenMain(
{ repository.getDorms() },
{ _dorms.value = it }
)
}
override fun onCleared() {
super.onCleared()
if(::job.isInitialized) job.cancel()
}
}
Interface:
interface RecyclerViewClickListener {
fun onRecyclerViewItemClick(view: View, dorms: Dorms)
}
Details Fragment:
class dormDetailsFragment : Fragment() {
companion object {
private const val DORMS = "model"
fun newInstance(dorms: Dorms): dormDetailsFragment{
val args = Bundle()
args.putSerializable(DORMS, dorms)
val fragment = dormDetailsFragment()
fragment.arguments = args
return fragment
}
}
private lateinit var viewModel: DormDetailsViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val fragmentDormDetailsBinding =
FragmentDormDetailsBinding.inflate(inflater,container,false)
val model = arguments!!.getSerializable(DORMS) as Dorms
fragmentDormDetailsBinding.dormDetails = model
return fragmentDormDetailsBinding.root
}
}
Home Fragment Layout
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/fragment_home">
<TextView
android:id="#+id/text_home"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/refreshLayout">
<androidx.recyclerview.widget.RecyclerView
tools:listitem="#layout/layout_home"
android:id="#+id/recyclerViewDorms"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Details Layout
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="dormDetails"
type="com.pptt.roomy.data.models.Dorms" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.pptt.roomy.ui.home.dormDetails.DormDetailsFragment"
android:id="#+id/DormDetailsFrag">
<ImageView
app:image="#{dormDetails.image}"
android:id="#+id/image"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:background="#drawable/propertysample"
/>
<TextView
android:text="#{String.valueOf(dormDetails.dormPrice)}"
tools:text="Php 2500"
android:id="#+id/textViewPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="6dp"
android:layout_marginStart="10dp"
android:textSize="20sp"
android:textStyle="normal"
android:textColor="#000000"
app:layout_constraintTop_toBottomOf="#id/image"
app:layout_constraintLeft_toLeftOf="parent" />
<TextView
android:text="#{dormDetails.dormName}"
tools:text="Dorm ni Jupa"
android:id="#+id/textViewPropertyName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="1dp"
android:layout_marginStart="10dp"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="#000000"
app:layout_constraintTop_toBottomOf="#id/textViewPrice"
app:layout_constraintLeft_toLeftOf="parent" />
<TextView
android:text="#{dormDetails.dormType}"
tools:text="1 BR with Dining and Kitchen"
android:id="#+id/textViewRoomType"
android:layout_below="#id/textViewPropertyName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:layout_marginLeft="40dp"
android:textSize="16sp"
app:layout_constraintTop_toBottomOf="#+id/textViewPropertyName"
app:layout_constraintLeft_toLeftOf="parent"/>
<TextView
android:text="#{dormDetails.dormAddress}"
android:id="#+id/textViewAddress"
android:layout_marginBottom="5dp"
tools:text="455 San Jose II St., Brgy. 425, Sampaloc, Manila"
android:textAppearance="#style/Base.TextAppearance.AppCompat.Small"
android:padding="5dp"
android:layout_marginLeft="40dp"
android:layout_width="wrap_content"
android:textAlignment="center"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#+id/textViewRoomType"
app:layout_constraintLeft_toLeftOf="parent"/>
<TextView
android:text="#{dormDetails.dormDetails}"
android:id="#+id/textViewDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="A very long textarea to contain dorm description. Should be multiline"
android:padding="5dp"
android:layout_marginLeft="20dp"
app:layout_constraintTop_toBottomOf="#id/textViewAddress"
app:layout_constraintLeft_toLeftOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Anything else that's needed will be edited for later.
Remove these lines:
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.add(R.id.fragment_home, HomeFragment.newInstance(), "dormList")
.commit()
}
You're adding one HomeFragment via the NavHostFragment and another manually. You don't need to manually add Fragment when using Navigation.
You should also be updating your onRecyclerViewItemClick to use navigate() as per the Navigate to a destination documentation:
override fun onRecyclerViewItemClick(view: View, dorms: Dorms) {
val navController = findNavController(R.id.nav_host_fragment)
// If you're using Safe Args, use the ID generated from
// the navigation graph and make sure you have
// an argument of the correct type
navController.navigate(
HomeFragmentDirections.actionHomeToDetails(dorms))
}
You might find it helpful to look at the Pass data between destinations documentation to see how to create an <argument> in your graph for your Dorms object and how to set up Safe Args to generate the Directions class for you.