1

I have an app which has one main activity and 3 fragments. Let's name them A, B and C as shown below:

fragments

  • Fragment A has a RecyclerView. When i click on an item in the RecyclerView, it passes an Account object in safe args to the Fragment B where it displays the data.

  • In Fragment B, the user can then press the edit button to edit the Account object, which then navigates him/her to Fragment C.

  • In fragment C, the user can either press the back button to cancel the modification and return to Fragment B, or the user can modify and press save to go back to Fragment A.

The problem here is that, if after pressing the edit button, the user perform some modifications and then press the back button without saving, it still modifies the object temporarily (temporarily because if i close the app and then open it again, the object resets to its original state.).

Below is my code (this is a simple code just for the sake of reproducing the issue):

Fragment A.kt

<androidx.constraintlayout.widget.ConstraintLayout
    android:id="@+id/Layout_Fragment_Account"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

<androidx.recyclerview.widget.RecyclerView
        android:id="@+id/RecyclerView_Account"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="@{ViewModel.loadingStatus==List.LIST ? View.VISIBLE : View.GONE}"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:visibility="gone" />

...

</androidx.constraintlayout.widget.ConstraintLayout>

Fragment A.kt

private fun subscribeAccounts(accounts: List<Account>) {
    val adapter = AccountAdapter(
        /* The click listener to handle account on clicks */
        AccountClickListener {
            navigateTo(AccountFragmentDirections.actionFragmentAccountToFragmentViewAccount(it))
        },
        /* The click listener to handle popup menu for each accounts */
        AccountOptionsClickListener { view, Account ->
            //View Popup
        }
    )

    binding.RecyclerViewAccount.apply {
        /*
        * State that layout size will not change for better performance
        */
        setHasFixedSize(true)

        /* Bind the layout manager */
        layoutManager = LinearLayoutManager(requireContext())

        /* Bind the adapter */
        this.adapter = adapter
    }

    /* Submits the list for displaying */
    adapter.submitList(accounts)
}

Fragment B.xml

<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"
        errorResource="@{@drawable/ic_account_placeholder}"
        imageUrl="@{Account.logoUrl}"
        loadingResource="@{@drawable/ic_image_loading}"
        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:text="@{Account.entryName}"
        android:textAlignment="center"
        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.constraintlayout.widget.ConstraintLayout>

</layout>

Fragment B.kt

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    /* Binds the UI */
    _binding = FragmentViewAccountBinding.inflate(inflater, container, false)
    /* Instantiate the view model */
    _viewModel = ViewModelProvider(this).get(ViewAccountViewModel::class.java)
    /* Bind lifecycle owner to this */
    binding.lifecycleOwner = this

    /*
    * Fetch the account object from argument
    * Then bind account object to layout
    */
    val account = ViewAccountFragmentArgs.fromBundle(requireArguments()).accountToVIEW
    binding.account = account
    _account = account

    /* Returns the root view */
    return 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) =
    when (item.itemId) {
        R.id.Action_Duplicate -> {
            navigateTo(
                ViewAccountFragmentDirections.actionFragmentViewAccountToFragmentAddAccount(
                    _account.apply {
                        this.id = 0
                    })
            )
            true
        }
        R.id.Action_Edit -> {
            navigateTo(
                ViewAccountFragmentDirections.actionFragmentViewAccountToFragmentAddAccount(
                    _account
                )
            )
            true
        }
        else -> false
    }

}

Fragment C.xml

<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="...Constants" />

    <variable
        name="ViewModel"
        type="...AddAccountViewModel" />
</data>

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingStart="16dp"
    android:paddingEnd="16dp"
    android:paddingBottom="16dp">

...

    <!--
    **************** Require Text Fields ****************
    -->
    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/Account_Name"
        style="@style/Locky.TextBox.Default"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:hint="@string/field_account_name"
        app:endIconMode="clear_text"
        app:helperText="@string/label_required"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/Barrier_Logo"
        app:startIconDrawable="@drawable/ic_profile">

        <com.google.android.material.textfield.TextInputEditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textCapWords"
            android:text="@={ViewModel.entryName}" />
    </com.google.android.material.textfield.TextInputLayout>

...

</LinearLayout>

</layout>

Fragment C.kt

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    _binding = FragmentAddAccountBinding.inflate(inflater, container, false)
    /* Binds the UI */
    _binding = FragmentAddAccountBinding.inflate(inflater, container, false)
    /* Instantiate the view model */
    _viewModel = ViewModelProvider(this).get(AddAccountViewModel::class.java)
    /* Bind view model to layout */
    binding.viewModel = viewModel
    /* Bind lifecycle owner to this */
    binding.lifecycleOwner = this

    /*
    * Fetch the account object from argument
    * And set it to view model for two way binding
    */
    val account = AddAccountFragmentArgs.fromBundle(requireArguments()).accountToADD
    viewModel.setAccount(account)
    /* Returns the root view */
    return binding.root
}

Fragment C view model

class AddAccountViewModel(application: Application) : ObservableViewModel(application) {

/**
 * Bindable two-way binding
 **/
private lateinit var _account: Account

var entryName: String
    @Bindable get() {
        return _account.entryName
    }
    set(value) {
        _account.entryName = value
        notifyPropertyChanged(BR.entryName)
    }

var logoUrl: String?
    @Bindable get() {
        return _account.logoUrl
    }
    set(value) {
        _account.logoUrl = value ?: ""
        notifyPropertyChanged(BR.logoUrl)
    }


internal fun setAccount(account: Account?) {
    this._account = account ?: Account()
}
}

navigation.xml

<fragment
    android:id="@+id/Fragment_A"
    android:name="...AccountFragment"
    android:label="Accounts"
    tools:layout="@layout/fragment_account">
    <action
        android:id="@+id/action_Fragment_Account_to_BottomSheet_Fragment_Account_Filter"
        app:destination="@id/BottomSheet_Fragment_Account_Filter" />
    <action
        android:id="@+id/action_Fragment_Account_to_Fragment_View_Account"
        app:destination="@id/Fragment_View_Account" />
</fragment>

<fragment
    android:id="@+id/Fragment_B"
    android:name="...ViewAccountFragment"
    android:label="View Account"
    tools:layout="@layout/fragment_view_account">
    <action
        android:id="@+id/action_Fragment_View_Account_to_Fragment_Add_Account"
        app:destination="@id/Fragment_Add_Account" />
    <argument
        android:name="ACCOUNT_toVIEW"
        app:argType="....Account" />
</fragment>

<fragment
    android:id="@+id/Fragment_C"
    android:name="...AddAccountFragment"
    android:label="Add Account"
    tools:layout="@layout/fragment_add_account">
    <action
        android:id="@+id/action_Fragment_Add_Account_to_BottomSheet_Fragment_Account_Logo"
        app:destination="@id/BottomSheet_Fragment_Account_Logo" />
    <argument
        android:name="ACCOUNT_ToADD"
        app:argType="...Account" />
</fragment>

Below is a demonstration of the issue:

issue demonstration

I've tried a quick hack where i save an instance of an unmodified account object and before the user leaves the fragment i reset the changes by assigning each variable but this is not efficient. I think i am doing something wrong here with the safe args?

Can someone please help me. I really can't figure this one out. Thank you

Mervin Hemaraju
  • 1,921
  • 2
  • 22
  • 71

0 Answers0