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.
Related
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) }
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 have a single activity with many fragments (Using jetpack navigation). On my first fragment, i have a recyclerview. If i scroll on the first fragment and then navigate to the other fragment, the fragment retains the scroll position and i don't want that. An example is as follows:
i.e. Suppose i have two fragments A and B, When my app starts it starts on A. Suppose i start scrolling on A and then navigate to B. My app retains the scroll position on B which is not what i want. I want fragment B to start on top. And then when it returns to fragment A, i want it to retain the scroll position it previously scrolled.
Fragment A.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>
<import type="android.view.View" />
<variable
name="ViewModel"
type="....AccountViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/Layout_Fragment_Account"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--
Recyclerview
-->
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/RecyclerView_Account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="#{ViewModel.accountListVisibility? View.VISIBLE : View.GONE}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone" />
<!--
Empty Views and group
-->
<androidx.constraintlayout.widget.Group
android:id="#+id/Empty_View"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{ViewModel.accountEmptyViewVisibility?
View.VISIBLE : View.GONE}"
app:constraint_referenced_ids="Empty_View_Illustration,Empty_View_Title,Empty_View_Subtitle" />
<ImageView
android:id="#+id/Empty_View_Illustration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:scaleType="centerCrop"
app:layout_constraintBottom_toTopOf="#+id/Empty_View_Title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="#drawable/il_account" />
<TextView
android:id="#+id/Empty_View_Title"
style="#style/Locky.Text.Title6"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:text="#string/text_title_emptyView_accounts"
android:textAlignment="center"
app:layout_constraintBottom_toTopOf="#id/Empty_View_Subtitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/Empty_View_Illustration"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent=".8" />
<TextView
android:id="#+id/Empty_View_Subtitle"
style="#style/Locky.Text.Subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="?attr/actionBarSize"
android:text="#string/text_subtitle_emptyView_accounts"
android:textAlignment="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.6"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/Empty_View_Title"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent=".8" />
<!--
Progress Bar
-->
<include
android:id="#+id/Progress_Bar"
layout="#layout/custom_view_list_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="#{ViewModel.loadingStatus? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Fragment A.kt:
class AccountFragment : Fragment() {
private var _binding: FragmentAccountBinding? = null
private var _viewModel: AccountViewModel? = null
private var _lastClickTime: Long = 0
private val binding get() = _binding!!
private val viewModel get() = _viewModel!!
companion object {
const val TAG = "ACCOUNT_FRAGMENT_DEBUG"
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
_binding = FragmentAccountBinding.inflate(inflater, container, false)
// Fetch view model
_viewModel = ViewModelProvider(this).get(AccountViewModel::class.java)
//Bind view model to layout
binding.viewModel = _viewModel
// Bind lifecycle owner
binding.lifecycleOwner = this
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
/* Hides the soft keyboard */
hideSoftKeyboard(binding.root)
/* Observe snack bar events */
observeSnackBarEvent()
/* Observe the account list changes */
observeAccounts()
/* Observe back stack entry result after navigating from sort sheet */
observeBackStackEntryForSortSheet()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_toolbar_filter, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.Toolbar_Filter -> {
navigateToSort()
true
}
else -> false
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
/*
* My Functions
*/
private fun observeBackStackEntryForSortSheet() {
val navController = findNavController()
// After a configuration change or process death, the currentBackStackEntry
// points to the dialog destination, so you must use getBackStackEntry()
// with the specific ID of your destination to ensure we always
// get the right NavBackStackEntry
val navBackStackEntry = navController.getBackStackEntry(R.id.Fragment_Account)
// Create our observer and add it to the NavBackStackEntry's lifecycle
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains(KEY_ACCOUNTS_SORT)
) {
viewModel.sortChange(
navBackStackEntry.savedStateHandle.get<AccountSort>(
KEY_ACCOUNTS_SORT
)!!
)
navBackStackEntry.savedStateHandle.remove<AccountSort>(KEY_ACCOUNTS_SORT)
}
}
navBackStackEntry.lifecycle.addObserver(observer)
// As addObserver() does not automatically remove the observer, we
// call removeObserver() manually when the view lifecycle is destroyed
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}
private fun observeSnackBarEvent() {
viewModel.showSnackBarEvent.observe(viewLifecycleOwner, Observer {
if (it != null) {
snackBarAction(it)
}
})
}
private fun observeAccounts() {
with(viewModel) {
accounts.observe(viewLifecycleOwner, Observer {
if (it != null) {
//set loading flag to hide loading animation
doneLoading()
//Alternate visibility for account list and empty view
alternateAccountListVisibility(it.size)
//Submit the cards
initiateAccounts().submitList(it)
}
})
}
}
private fun initiateAccounts(): AccountAdapter {
val adapter = AccountAdapter(
AccountClickListener {
navigateToSelectedAccount(it)
},
AccountOptionsClickListener { view, card ->
view.apply {
isEnabled = false
}
createPopupMenu(view, card)
})
binding.RecyclerViewAccount.apply {
this.adapter = adapter
setHasFixedSize(true)
}
return adapter
}
private fun createPopupMenu(view: View, account: Account) {
requireContext().createPopUpMenu(
view,
R.menu.menu_moreoptions_account,
PopupMenu.OnMenuItemClickListener {
when (it.itemId) {
R.id.Menu_CopyUsername -> copyToClipboardAndToast(account.username)
R.id.Menu_CopyPass -> copyToClipboardAndToast(account.password)
R.id.Menu_ShowPass -> triggerSnackBarEvent(account.password)
else -> false
}
}, PopupMenu.OnDismissListener {
view.apply {
isEnabled = true
}
})
}
private fun navigateToSort() {
if (SystemClock.elapsedRealtime() - _lastClickTime >= 800) {
_lastClickTime = SystemClock.elapsedRealtime()
navigateTo(AccountFragmentDirections.actionFragmentAccountToBottomSheetFragmentAccountFilter())
}
}
private fun navigateToSelectedAccount(account: Account) {
navigateTo(
AccountFragmentDirections.actionFragmentAccountToFragmentViewAccount(
account
)
)
}
private fun snackBarAction(message: String) {
binding.LayoutFragmentAccount.snackbar(message) {
action(getString(R.string.button_snack_action_close)) { dismiss() }
}
viewModel.doneShowingSnackBar()
}
private fun triggerSnackBarEvent(message: String): Boolean {
viewModel.setSnackBarMessage(message)
return true
}
private fun copyToClipboardAndToast(message: String): Boolean {
copyToClipboard(message)
toast(getString(R.string.message_copy_successful))
return true
}
Fragment B.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="Account"
type="....Account" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/Layout_Credential_View"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/Account_Logo"
imageUrl="#{Account.logoUrl}"
loadingResource="#{#drawable/ic_image_loading}"
errorResource="#{#drawable/ic_account_placeholder}"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="#drawable/ic_account_placeholder" />
<TextView
android:id="#+id/Account_Name"
style="#style/Locky.Text.Title5.Name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:textAlignment="center"
android:text="#{Account.accountName}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/Account_Logo"
tools:text="This can be a very very very long title toooooo" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/RecyclerView_Credentials_Field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="8dp"
android:nestedScrollingEnabled="true"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/Account_Name" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Fragment B.kt
class ViewAccountFragment : Fragment() {
private var _binding: FragmentViewAccountBinding? = null
private var _viewModel: ViewAccountViewModel? = null
private lateinit var _account: Account
private val binding get() = _binding!!
private val viewModel get() = _viewModel!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//Fetch the layout and do the binding
_binding = FragmentViewAccountBinding.inflate(inflater, container, false)
//Instantiate view model
_viewModel = ViewModelProvider(this).get(ViewAccountViewModel::class.java)
binding.lifecycleOwner = this
//Fetch the account clicked on the previous screen
_account = ViewAccountFragmentArgs.fromBundle(requireArguments()).parcelcredaccount
with(_account) {
//Bind the account to the layout for displaying
binding.account = this
//Submit the account details to the recyclerview
initiateCredentialsFieldList().submitList(viewModel.fieldList(this))
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
/* Hides the soft keyboard */
hideSoftKeyboard(binding.root)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_credentials_actions, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.Action_Duplicate -> {
/*
* We set the account id to empty here
* When the add screen receives it, it wil perceive it as a new account that needs to be
* added to the database
*/
navigateToEditScreen(_account.copy(accountID = generateUniqueID()))
true
}
R.id.Action_Edit -> {
navigateToEditScreen(_account)
true
}
R.id.Action_Delete -> {
deleteConfirmationDialog(_account.accountName)
true
}
else -> false
}
}
private fun initiateCredentialsFieldList(): CredentialsViewAdapter {
val credentialsAdapter =
CredentialsViewAdapter(
CopyClickListener { data ->
copyToClipboardAndToast(data)
},
ViewClickListener { data ->
snackBarAction(data)
})
binding.RecyclerViewCredentialsField.apply {
adapter = credentialsAdapter
setHasFixedSize(true)
}
return credentialsAdapter
}
private fun deleteAndNavigateBackToAccountList() {
with(_account) {
viewModel.delete(accountID)
toast(getString(R.string.message_credentials_deleted, accountName))
findNavController().popBackStack()
}
}
navigation.xml
<fragment
android:id="#+id/Fragment_Account"
android:name="....AccountFragment"
android:label="Accounts"
tools:layout="#layout/fragment_account">
<action
android:id="#+id/action_Fragment_Account_to_Fragment_View_Account"
app:destination="#id/Fragment_View_Account" />
<action
android:id="#+id/action_Fragment_Account_to_BottomSheet_Fragment_Account_Filter"
app:destination="#id/BottomSheet_Fragment_Account_Filter" />
</fragment>
MainActivity.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">
<androidx.drawerlayout.widget.DrawerLayout
android:id="#+id/Drawer_Main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.main.MainActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="#+id/Layout_Coordinator_Main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="#+id/Toolbar_Main"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#color/colorOnSurface"
android:outlineAmbientShadowColor="#color/colorShadowColor"
android:outlineSpotShadowColor="#color/colorShadowColor">
<TextView
android:id="#+id/Toolbar_Main_Title"
style="#style/Locky.Text.Title6.Toolbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="#string/app_name" />
</com.google.android.material.appbar.MaterialToolbar>
<androidx.core.widget.NestedScrollView
android:id="#+id/Nested_Scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize"
android:fillViewport="true">
<fragment
android:id="#+id/Navigation_Host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/navigation_drawer_main" />
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/FAB_Search"
style="#style/Locky.FloatingActionButton.Mini"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:layout_marginBottom="85dp"
app:layout_anchor="#id/FAB_Add"
app:layout_anchorGravity="top|center_horizontal"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
app:srcCompat="#drawable/ic_search" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/FAB_Add"
style="#style/Locky.FloatingActionButton.Normal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="#dimen/fab_margin"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
app:srcCompat="#drawable/ic_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="#+id/Navigation_View"
style="#style/Locky.Widget.Custom.NavigationView"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:clipToPadding="false"
android:paddingStart="0dp"
android:paddingEnd="16dp"
app:headerLayout="#layout/drawer_header"
app:itemTextAppearance="#style/Locky.Text.Body.Drawer"
app:menu="#menu/menu_drawer_main" />
</androidx.drawerlayout.widget.DrawerLayout>
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var _binding: ActivityMainBinding
private lateinit var _viewModel: MainActivityViewModel
private lateinit var _appBarConfiguration: AppBarConfiguration
//Fragments that can navigate with the drawer
private val _navigationFragments = setOf(
R.id.Fragment_Card,
R.id.Fragment_Account,
R.id.Fragment_Device
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
_viewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java)
_binding.lifecycleOwner = this
//Set the support action bar to the toolbar
setSupportActionBar(_binding.ToolbarMain)
//Remove the default actionbar title
supportActionBar?.setDisplayShowTitleEnabled(false)
/* Updates the app settings*/
updateAppSettings()
//Setup the navigation components
navigationUISetup()
//Load FABs
listenerForAddFab()
listenerForSearchFab()
//Scroll changes to adjust toolbar elevation accordingly
setUpNestedScrollChangeListener()
}
override fun onOptionsItemSelected(item: MenuItem) =
item.onNavDestinationSelected(findNavController(R.id.Navigation_Host)) || super.onOptionsItemSelected(
item
)
override fun onSupportNavigateUp() =
findNavController(R.id.Navigation_Host).navigateUp(_appBarConfiguration)
override fun finish() {
super.finish()
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
}
private fun navigationUISetup() {
//Fetch the Nav Controller
val navController = findNavController(R.id.Navigation_Host)
//Setup the App Bar Configuration
_appBarConfiguration = AppBarConfiguration(_navigationFragments, _binding.DrawerMain)
//Use Navigation UI to setup the app bar config and navigation view
NavigationUI.setupActionBarWithNavController(this, navController, _appBarConfiguration)
NavigationUI.setupWithNavController(_binding.NavigationView, navController)
//Set the mini FABs with navigation to navigate to fragments accordingly.
Navigation.setViewNavController(_binding.FABAdd, navController)
Navigation.setViewNavController(_binding.FABSearch, navController)
//Add on change destination listener to navigation controller to handle fab visibility
navigationDestinationChangeListener_FAB(navController)
//Add on change destination listener to navigation controller to handle screen title visibility
navigationDestinationChangeListener_ToolbarTitle(navController)
}
private fun setUpNestedScrollChangeListener() =
_binding.NestedScroll.setOnScrollChangeListener { _, _, scrollY, _, _ ->
if (scrollY > 0) {
_binding.ToolbarMain.elevation = 12F
} else {
_binding.ToolbarMain.elevation = 0F
}
}
private fun navigationDestinationChangeListener_ToolbarTitle(navController: NavController) {
navController.addOnDestinationChangedListener { _, nd, _ ->
when (nd.id) {
R.id.Fragment_Account -> updateToolbar(getString(R.string.text_title_screen_accounts))
R.id.Fragment_Card -> updateToolbar(getString(R.string.text_title_screen_cards))
R.id.Fragment_Device -> updateToolbar(getString(R.string.text_title_screen_devices))
R.id.Fragment_Settings -> updateToolbar(getString(R.string.text_title_screen_settings))
R.id.Fragment_Profile -> updateToolbar(getString(R.string.text_title_screen_profile))
R.id.Fragment_About -> updateToolbar(getString(R.string.text_title_screen_about))
R.id.Fragment_Donate -> updateToolbar(getString(R.string.text_title_screen_donate))
else -> {
//Show the toolbar
updateToolbar(null)
}
}
}
}
private fun navigationDestinationChangeListener_FAB(navController: NavController) {
navController.addOnDestinationChangedListener { nc, nd, _ ->
when (nd.id) {
nc.graph.startDestination,
R.id.Fragment_Card,
R.id.Fragment_Device -> {
_binding.DrawerMain.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
//Show all the FABs
showFABs()
}
else -> {
_binding.DrawerMain.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
//Hide all the FABs
hideFABs()
}
}
}
}
private fun getFadeNavOptions(): NavOptions? {
return NavOptions.Builder()
.setEnterAnim(R.anim.anim_fade_in)
.setExitAnim(R.anim.anim_fade_out)
.build()
}
private fun hideFABs() {
_binding.FABSearch.hide()
_binding.FABAdd.hide()
}
private fun showFABs() {
_binding.FABSearch.show()
_binding.FABAdd.show()
showFABFromSlidingBehavior(_binding.FABSearch, _binding.FABSearch.isVisible)
showFABFromSlidingBehavior(_binding.FABAdd, _binding.FABAdd.isVisible)
}
private fun showFABFromSlidingBehavior(fab: FloatingActionButton, isVisible: Boolean) {
val layoutParams: ViewGroup.LayoutParams = fab.layoutParams
if (layoutParams is CoordinatorLayout.LayoutParams) {
val behavior = layoutParams.behavior
if (behavior is HideBottomViewOnScrollBehavior) {
if (isVisible) {
behavior.slideUp(fab)
} else {
behavior.slideDown(fab)
}
}
}
}
I have attached a gif to demontstrate the issue here:
In the GIF i navigate from 3 fragments (Fragment A > Fragment B > Fragment C)
Is there anything i am doing wrong here ?
you have the same layoutmanager for both fragments, when you populate your different fragments; the same layoutmanager is called. Which then tries to restore the same position thinking its the same recyclerview, which is kind of a feature when you think about it.
from the docs:
Called when the RecyclerView is ready to restore the state based on a
previous RecyclerView. Notice that this might happen after an actual
layout, based on how Adapter prefers to restore State. See
RecyclerView.Adapter.getStateRestorationPolicy()
which means what we need is not to restore the state which can be done by passing
PREVENT to RecyclerView.Adapter.StateRestorationPolicy
solution1: in your fragment B adapter just call adapter.stateRestorationPolicy = PREVENT
solution2: create a different layoutmanager for fragment B in case you want to restore position for something else.
EDIT :: QA :: how can i set the view to be on top (Near Status Bar) :
Well, since you are populating your fragments inside a NestedScrollView you should call NestedScrollView.scrollTo(0, 0); when you navigate to the required fragment probably by waiting on a callback from addOnDestinationChangedListener inside your MainActivity.kt
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 see the problem with a RecyclerView in viewpager fragment and I use bottom naviagation bar. When I tab on the navigation bar to change a screen and then pressed on back button it will go to the first screen which have the viewpager but the recyclerview doesn't appear and when I tab the naviagtion bar to reselect this screen it appear.
The recyclerview is show the list from firestore database and using ViewModel.
I try to set retainInstance to true but it doesn't work. And try to setup the recyclerview in onCreate and onCreateView it doesn't work too.
Edit: this is my code.
MainActivity.kt
class MainActivity : AppCompatActivity() {
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)
navView.setupWithNavController(navController)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId){
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
}
activity_main.xml
<?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"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="#drawable/bg_nav"
app:itemIconTint="#color/navbar_color"
app:itemTextColor="#color/navbar_color"
app:itemTextAppearanceActive="#style/navbar_font"
app:itemTextAppearanceInactive="#style/navbar_font"
app:itemBackground="#drawable/nav_ripple"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="#menu/bottom_nav_menu"/>
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="#id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
HomeFragement.kt (it contain tablayout and viewpager)
class HomeFragment : Fragment() {
private lateinit var homeViewModel: HomeViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
homeViewModel =
ViewModelProviders.of(this).get(HomeViewModel::class.java)
val root = inflater.inflate(R.layout.fragment_home, container, false)
return root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupTab()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
private fun setupTab(){
Log.d("SET UP TAB", "SETTING UP")
val pageTabAdapter = PageTabAdapter(activity!!.supportFragmentManager, activity_tab.tabCount)
view_activity.adapter = pageTabAdapter
view_activity.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(activity_tab))
activity_tab.setOnTabSelectedListener(object : TabLayout.OnTabSelectedListener{
override fun onTabReselected(p0: TabLayout.Tab?) {
}
override fun onTabUnselected(p0: TabLayout.Tab?) {
}
override fun onTabSelected(p0: TabLayout.Tab?) {
view_activity.currentItem = p0!!.position
}
})
}
}
fragment_home.xml
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/white"
android:orientation="vertical">
<TextView
android:id="#+id/home_header"
style="#style/page_header"
android:text="#string/title_home"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="#+id/calendar_btn"
android:layout_width="60dp"
android:layout_height="40dp"
android:background="#drawable/btn_calendar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="#id/home_header"
app:layout_constraintBottom_toBottomOf="#id/home_header"
android:layout_marginEnd="10dp"/>
<com.google.android.material.tabs.TabLayout
android:id="#+id/activity_tab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabRippleColor="#color/colorPrimary"
app:tabTextAppearance="#style/tab_font"
app:tabIndicatorHeight="2.5dp"
app:tabGravity="fill"
app:layout_constraintTop_toBottomOf="#id/home_header"
app:layout_constraintStart_toStartOf="parent"
android:background="#android:color/white">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="กิจกรรมในเดือนนี้" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="กิจกรรมที่ผ่านไปแล้ว" />
</com.google.android.material.tabs.TabLayout>
<androidx.viewpager.widget.ViewPager
android:id="#+id/view_activity"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="#id/activity_tab"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
PageAdapter.kt
class PageTabAdapter(private val fragmentManager: FragmentManager, private val anInt: Int) :
FragmentStatePagerAdapter(fragmentManager) {
override fun getItem(position: Int): Fragment {
return when (position) {
0 -> {
NewActivity.newInstance()
}
1 -> {
PastActivity.newInstance()
}
else -> {
null!!
}
}
}
override fun getCount(): Int {
return anInt
}
}
ActivityAdapter.kt (this is a recyclerview adapter)
class ActivityAdapter(
private val mContext: Context, private val mItems: List<ActivityModel>
): RecyclerView.Adapter<ActivityAdapter.Holder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val inflater = LayoutInflater.from(this.mContext)
val view = inflater.inflate(R.layout.activity_list, parent, false)
return Holder(view)
}
override fun getItemCount(): Int {
return this.mItems.size
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val item = this.mItems[position]
holder.setName(item.name)
val date = item.date?.toDate()
val dateFormat: DateFormat = SimpleDateFormat("dd-MMM-yyyy")
val newDate = dateFormat.format(date).toString()
holder.setDate(newDate)
val period = item.start + " - " +item.end
holder.setPeriod(period)
holder.setButton(item)
}
inner class Holder(view: View): RecyclerView.ViewHolder(view){
private var activityName: TextView = view.activity_list_name
private var activityDate: TextView = view.activity_list_date
private var activityPeriod: TextView = view.activity_list_period
private var activityPicture: ImageView = view.activity_list_pic
private var activityButton: Button = view.activity_list_btn
fun setName(name: String?){
this.activityName.text = name
}
fun setDate(date: String?){
this.activityDate.text = date
}
fun setPeriod(period: String?){
this.activityPeriod.text = period
}
fun setPicture(picpath: String?){
}
fun setButton(data: ActivityModel){
}
}
}
}
PS.: I'm using Kotlin in my project
Thank you