Android single activity with multiple fragment architecture
This post will demonstrate multiple fragments in an activity, a shared ViewModel across fragments, data binding, LiveData, and the Jetpack Navigation component.
1. First thing is to have a navigation layout file for setting up the fragments and navigation actions, create a xml file at navigation/nav_graph.xml with the following.
2. The activity_main.xml will be the root layout for the MainActivity and it will use the nav_graph.xml above to set up the navigation.
3. Configure the MainActivity with the layouts and navigations.
import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupActionBarWithNavController class MainActivity : AppCompatActivity(R.layout.activity_main) { private lateinit var navController: NavController override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Retrieve NavController from the NavHostFragment val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment navController = navHostFragment.navController // Set up the action bar for use with the NavController setupActionBarWithNavController(navController) } /** * Handle navigation when the user chooses Up from the action bar. */ override fun onSupportNavigateUp(): Boolean { return navController.navigateUp() || super.onSupportNavigateUp() } }
4. Crate a ViewModel class for sharing data between fragments, all of the fragments in this example will get and set data through this ViewModel via LiveData.
import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import java.text.NumberFormat import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale /** Price for a single cupcake */ private const val PRICE_PER_CUPCAKE = 2.00 /** Additional cost for same day pickup of an order */ private const val PRICE_FOR_SAME_DAY_PICKUP = 3.00 /** * [OrderViewModel] holds information about a cupcake order in terms of quantity, flavor, and * pickup date. It also knows how to calculate the total price based on these order details. */ class OrderViewModel : ViewModel() { // Quantity of cupcakes in this order private val _quantity = MutableLiveData() val quantity: LiveData = _quantity // Cupcake flavor for this order private val _flavor = MutableLiveData () val flavor: LiveData = _flavor // Possible date options val dateOptions: List = getPickupOptions() // Pickup date private val _date = MutableLiveData () val date: LiveData = _date // Price of the order so far private val _price = MutableLiveData () val price: LiveData = Transformations.map(_price) { // Format the price into the local currency and return this as LiveData NumberFormat.getCurrencyInstance().format(it) } init { // Set initial values for the order resetOrder() } /** * Set the quantity of cupcakes for this order. * * @param numberCupcakes to order */ fun setQuantity(numberCupcakes: Int) { _quantity.value = numberCupcakes updatePrice() } /** * Set the flavor of cupcakes for this order. Only 1 flavor can be selected for the whole order. * * @param desiredFlavor is the cupcake flavor as a string */ fun setFlavor(desiredFlavor: String) { _flavor.value = desiredFlavor } /** * Set the pickup date for this order. * * @param pickupDate is the date for pickup as a string */ fun setDate(pickupDate: String) { _date.value = pickupDate updatePrice() } /** * Returns true if a flavor has not been selected for the order yet. Returns false otherwise. */ fun hasNoFlavorSet(): Boolean { return _flavor.value.isNullOrEmpty() } /** * Reset the order by using initial default values for the quantity, flavor, date, and price. */ fun resetOrder() { _quantity.value = 0 _flavor.value = "" _date.value = dateOptions[0] _price.value = 0.0 } /** * Updates the price based on the order details. */ private fun updatePrice() { var calculatedPrice = (quantity.value ?: 0) * PRICE_PER_CUPCAKE // If the user selected the first option (today) for pickup, add the surcharge if (dateOptions[0] == _date.value) { calculatedPrice += PRICE_FOR_SAME_DAY_PICKUP } _price.value = calculatedPrice } /** * Returns a list of date options starting with the current date and the following 3 dates. */ private fun getPickupOptions(): List { val options = mutableListOf () val formatter = SimpleDateFormat("E MMM d", Locale.getDefault()) val calendar = Calendar.getInstance() repeat(4) { options.add(formatter.format(calendar.time)) calendar.add(Calendar.DATE, 1) } return options } }
5. Create a layout file for the first fragment StartFragment, fragment_start.xml. This layout declares a reference to the StartFragment and directly calls on the functions from StartFragment when a button is clicked.
5. The StartFragment class.
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.example.cupcake.databinding.FragmentStartBinding
import com.example.cupcake.model.OrderViewModel
/**
* This is the first screen of the Cupcake app. The user can choose how many cupcakes to order. When a button is clicked, it sets the quantity value in the shared view model and then navigates to the next fragment by findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
, where R.id.action_startFragment_to_flavorFragment is the action id defined in nav_graph.xml
*/
class StartFragment : Fragment() {
// Binding object instance corresponding to the fragment_start.xml layout
// This property is non-null between the onCreateView() and onDestroyView() lifecycle callbacks,
// when the view hierarchy is attached to the fragment.
private var binding: FragmentStartBinding? = null
// Use the 'by activityViewModels()' Kotlin property delegate from the fragment-ktx artifact
private val sharedViewModel: OrderViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val fragmentBinding = FragmentStartBinding.inflate(inflater, container, false)
binding = fragmentBinding
return fragmentBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.startFragment = this
}
/**
* Start an order with the desired quantity of cupcakes and navigate to the next screen.
*/
fun orderCupcake(quantity: Int) {
// Update the view model with the quantity
sharedViewModel.setQuantity(quantity)
// If no flavor is set in the view model yet, select vanilla as default flavor
if (sharedViewModel.hasNoFlavorSet()) {
sharedViewModel.setFlavor(getString(R.string.vanilla))
}
// Navigate to the next destination to select the flavor of the cupcakes
findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
/**
* This fragment lifecycle method is called when the view hierarchy associated with the fragment
* is being removed. As a result, clear out the binding object.
*/
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
}
6. Create a layout file for the FlavorFragment, fragment_flavor.xml. This layout file declared references for the shared ViewModel and FlavorFragment. It directly gets the data from the shared view model and displays it, and it directly calls on the functions from the FlavorFragment and ViewModel when a button or a radio button is interacted with by the user.
7. The fragment class for the FlavorFragment.
import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.example.cupcake.databinding.FragmentFlavorBinding import com.example.cupcake.model.OrderViewModel /** * [FlavorFragment] allows a user to choose a cupcake flavor for the order. */ class FlavorFragment : Fragment() { // Binding object instance corresponding to the fragment_flavor.xml layout // This property is non-null between the onCreateView() and onDestroyView() lifecycle callbacks, // when the view hierarchy is attached to the fragment. private var binding: FragmentFlavorBinding? = null // Use the 'by activityViewModels()' Kotlin property delegate from the fragment-ktx artifact private val sharedViewModel: OrderViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val fragmentBinding = FragmentFlavorBinding.inflate(inflater, container, false) binding = fragmentBinding return fragmentBinding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding?.apply { // Specify the fragment as the lifecycle owner lifecycleOwner = viewLifecycleOwner // Assign the view model to a property in the binding class viewModel = sharedViewModel // Assign the fragment flavorFragment = this@FlavorFragment } } /** * Navigate to the next screen to choose pickup date. */ fun goToNextScreen() { findNavController().navigate(R.id.action_flavorFragment_to_pickupFragment) } /** * Cancel the order and start over. */ fun cancelOrder() { // Reset order in view model sharedViewModel.resetOrder() // Navigate back to the [StartFragment] to start over findNavController().navigate(R.id.action_flavorFragment_to_startFragment) } /** * This fragment lifecycle method is called when the view hierarchy associated with the fragment * is being removed. As a result, clear out the binding object. */ override fun onDestroyView() { super.onDestroyView() binding = null } }
Search within Codexpedia
Search the entire web