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