Navigation Component is one of the Android jetpacks library. Its purpose is to have only one activity that serves as an entry point to your application using fragment for all UI screen. It comes with a lot of benefits which includes handling fragment transaction,control back stack by default, viewmodel support which I'm going to be explaining in this post
This post demonstrates how to pass data between two fragments using both viewmodel and Bundle.
Bundle is used to pass data between both activities and fragments, it maps values to String keys and then uses the key to retrieve the value.
Viewmodel is a helper class designed to manage UI related data in a life-cycle conscious way.It is responsible for preparing data for the UI and therefore helps to separate the view from business logics. Fragments can share a viewmodel using their activity scope to handle communication between each other.
The project sample fetches data from https://restcountries.eu/rest/v2/all which displays all countries on the screen, when the user clicks on each country it opens up the detail fragment showing some information about the country.This project is in kotlin and the final ui is shown below
To use Navigation component we need to add its dependency to build.gradle, I will also be listing other dependencies used in the project. First apply kotlin kapt at the top of the gradle file apply plugin: 'kotlin-kapt'
then add the following dependencies
Dependencies
//svgLoader implementation 'com.github.ar-android:AndroidSvgLoader:1.0.2' //navigation component implementation 'androidx.navigation:navigation-fragment-ktx:2.2.1' implementation 'androidx.navigation:navigation-ui-ktx:2.2.1' //viewpager2 implementation "androidx.viewpager2:viewpager2:1.0.0" implementation 'com.google.android.material:material:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.cardview:cardview:1.0.0' //retrofit implementation 'com.squareup.retrofit2:converter-gson:2.7.1' implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.squareup.retrofit2:retrofit:2.7.1' //rxjava2 implementation 'io.reactivex.rxjava2:rxjava:2.2.13' implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0' def lifecycle_version = "2.2.0" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" kapt "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
RetrofitService.kt
package kulloveth.developer.com.countrydetails.api import io.reactivex.Single import kulloveth.developer.com.countrydetails.data.model.CountryDetails import retrofit2.Response import retrofit2.Retrofit import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.GET interface RetrofitService { @GET("rest/v2/all") fun fetchCharacterName(): Single<Response<List<CountryDetails>>> companion object { fun getRetrofitInstance(): RetrofitService { val retrofit: Retrofit = Retrofit.Builder() .baseUrl("https://restcountries.eu/") .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build() return retrofit.create(RetrofitService::class.java)}} }
The above class is using the retrofit @Get annotation to pull the data from the Url
CountryDetails.kt
package kulloveth.developer.com.countrydetails.data.model import com.google.gson.annotations.Expose import com.google.gson.annotations.SerializedName import java.io.Serializable data class CountryDetails( val id: Int, @SerializedName("name") @Expose val name: String, @SerializedName("flag") @Expose val flag: String, val timezones : ArrayList<String>, @SerializedName("translations") val translations: Translations, @SerializedName("languages") val language: List<Language> ) : Serializable
Language.kt
package kulloveth.developer.com.countrydetails.data.model import com.google.gson.annotations.Expose import com.google.gson.annotations.SerializedName import java.io.Serializable data class CountryDetails( val id: Int, @SerializedName("name") @Expose val name: String, @SerializedName("flag") @Expose val flag: String, val timezones : ArrayList<String>, @SerializedName("translations") val translations: Translations, @SerializedName("languages") val language: List<Language> ) : Serializable
Translations.kt
package kulloveth.developer.com.countrydetails.data.model import java.io.Serializable data class Translations( val de: String, val es: String, val fr: String, val ja: String, val it: String, val br: String, val pt: String, val nl: String, val hr: String, val fa: String ) : Serializable
The above classes are the data class for representing the data structure present in the api also called model class
CountryDetailsRepository.kt
package kulloveth.developer.com.countrydetails.data import android.util.Log import androidx.lifecycle.MutableLiveData import io.reactivex.SingleObserver import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import kulloveth.developer.com.countrydetails.api.RetrofitService import kulloveth.developer.com.countrydetails.data.model.CountryDetails import retrofit2.Response class CountryDetailsRepository { val countrysLiveData: MutableLiveData<List<CountryDetails>> by lazy { MutableLiveData<List<CountryDetails>>().also { fetchCountryDetails() } } fun fetchCountryDetails() { val api = RetrofitService.getRetrofitInstance().fetchCharacterName() api.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : SingleObserver<Response<List<CountryDetails>>> { override fun onSuccess(t: Response<List<CountryDetails>>) { countrysLiveData.value = t.body() Log.d("rest", "" + countrysLiveData) } override fun onSubscribe(d: Disposable) {} override fun onError(e: Throwable) {}}) }}
CountrysViewModel.kt
package kulloveth.developer.com.countrydetails.ui.countrys import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import kulloveth.developer.com.countrydetails.data.CountryDetailsRepository import kulloveth.developer.com.countrydetails.data.model.CountryDetails import kulloveth.developer.com.countrydetails.data.model.Language import kulloveth.developer.com.countrydetails.data.model.Translations class CountrysViewModel : ViewModel() { private var countrysLiveData = MutableLiveData<List<CountryDetails>>() var translationsLiveData = MutableLiveData<Translations>() var languageLiveData = MutableLiveData<List<Language>>() fun fetchCountrys(): LiveData<List<CountryDetails>> { countrysLiveData = CountryDetailsRepository().countrysLiveData return countrysLiveData } fun setTranslations(translations: Translations) { translationsLiveData.value = translations } fun setLanguages(language: List<Language>) { languageLiveData.value = language }}
The above class extends the viewmodel class and contains the data to be observed by the view classes. The methods to take note of here is the setTranslations and setLanguages which am going to use to pass data between CountrysFragment where the data is been setup to both the TranslationsFragment and the LanguagesFragment where the data is been observed
MainViewModelFactory.kt
package kulloveth.developer.com.countrydetails.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import kulloveth.developer.com.countrydetails.ui.countrys.CountrysViewModel class MainViewModelFactory : ViewModelProvider.NewInstanceFactory() { override fun <T : ViewModel?> create(modelClass: Class<T>): T { return CountrysViewModel() as T } }
activity_main.xml
<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" tools:context=".MainActivity"> <fragment android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:defaultNavHost="true" app:navGraph="@navigation/nav_graph" /> </androidx.constraintlayout.widget.ConstraintLayout>
Three important points to note in the layout above is android:name which contains the class name of your Nav Host implementation, app:navGraph which associates the navHostFragment with a nav-graph where the user destinations are been specified, app:defaultNavHost="true" which ensures that your NavHostFragment intercepts the system Back button.
To add a navigation graph to your project, do the following:
In the Project window, right-click on the res directory and select New > Android Resource File. The New Resource File dialog appears.
Type a name in the File name field, such as "nav_graph".
Select Navigation from the Resource type drop-down list, and then click OK. A navigation resource directory will be automatically created in the res directory and there you find you nav-graph
nav-graph.xml
<?xml version="1.0" encoding="utf-8"?> <navigation 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:id="@+id/nav_graph" app:startDestination="@id/countrysFragment"> <fragment android:id="@+id/countrysFragment" android:name="kulloveth.developer.com.countrydetails.ui.countrys.CountrysFragment" android:label="fragment_countrys" tools:layout="@layout/fragment_countrys" > <action android:id="@+id/action_countrysFragment_to_detailsFragment" app:destination="@id/detailsFragment" /> </fragment> <fragment android:id="@+id/detailsFragment" android:name="kulloveth.developer.com.countrydetails.ui.details.DetailsFragment" android:label="fragment_details" tools:layout="@layout/fragment_details" > <argument android:name="countryName" app:argType="kulloveth.developer.com.countrydetails.data.model.CountryDetails" app:nullable="false" /> <argument android:name="countryFlag" app:argType="kulloveth.developer.com.countrydetails.data.model.CountryDetails" app:nullable="false" /> <argument android:name="timeZone" app:argType="kulloveth.developer.com.countrydetails.data.model.CountryDetails" app:nullable="false" /> </fragment> </navigation>
In the above nav-graph take note of the tags inside the detailsFragment which contains the details of the data to be received by the fragment. android:name is the String key used to identify a particular data while passing and receiving, app:argType is used to represent what type of data is been passed, app:nullable is used to indicate whether the data can be null or not
MainActivity.kt
package kulloveth.developer.com.countrydetails import android.os.Bundle import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) }}
fragment_countrys.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.countrys.CountrysFragment"> <androidx.recyclerview.widget.RecyclerView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/countryRv" /> </FrameLayout>
CountrysAdapter.kt
package kulloveth.developer.com.countrydetails.ui.countrys import android.app.Activity import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.ahmadrosid.svgloader.SvgLoader import kotlinx.android.synthetic.main.rv_list_item.view.* import kulloveth.developer.com.countrydetails.R import kulloveth.developer.com.countrydetails.data.model.CountryDetails class CountrysAdapter : ListAdapter<CountryDetails, CountrysAdapter.MainViewHolder>( DiffCallback() ) { lateinit var mItemCLicked: ItemCLickedListener class DiffCallback : DiffUtil.ItemCallback<CountryDetails>() { override fun areItemsTheSame(oldItem: CountryDetails, newItem: CountryDetails): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: CountryDetails, newItem: CountryDetails): Boolean { return oldItem.id == newItem.id }} override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder{ val view = LayoutInflater.from(parent.context).inflate(R.layout.rv_list_item, parent, false) return MainViewHolder(view)} override fun onBindViewHolder(holder: MainViewHolder, position: Int) { holder.bind(getItem(position)) holder.itemView.setOnClickListener { mItemCLicked.let { mItemCLicked.onItemClicked(getItem(position))}}} fun setUpListener(itemCLicked: ItemCLickedListener){ mItemCLicked = itemCLicked} class MainViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(countryDetails: CountryDetails) { itemView.country.text = countryDetails.name SvgLoader.pluck() .with(itemView.context as Activity?) .setPlaceHolder(R.mipmap.ic_launcher, R.mipmap.ic_launcher) .load(countryDetails.flag, itemView.flag)}} interface ItemCLickedListener { fun onItemClicked(countryDetails: CountryDetails)}}
The above class is an adapter class used to setup data for the recyclerView present in the CountrysFragment.kt
rv_list_item.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.cardview.widget.CardView 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="wrap_content" android:layout_margin="8dp" app:cardUseCompatPadding="true"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/flag" android:layout_width="wrap_content" android:layout_height="100dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/country" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:textColor="#08141a" app:layout_constraintEnd_toEndOf="@id/flag" app:layout_constraintStart_toStartOf="@id/flag" app:layout_constraintTop_toBottomOf="@id/flag" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.cardview.widget.CardView>
The recyclerview item layout used for the adapter viewHolder
CountrysFragment.kt
package kulloveth.developer.com.countrydetails.ui.countrys import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.navigation.NavController import androidx.navigation.Navigation import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.StaggeredGridLayoutManager import kotlinx.android.synthetic.main.fragment_countrys.* import kulloveth.developer.com.countrydetails.R import kulloveth.developer.com.countrydetails.data.model.CountryDetails import kulloveth.developer.com.countrydetails.ui.MainViewModelFactory class CountrysFragment : Fragment() { val adapter = CountrysAdapter() var navController: NavController? = null private val viewModelFactory = MainViewModelFactory() private lateinit var viewModel: CountrysViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initViewModel() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_countrys, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) navController = Navigation.findNavController(view) countryRv.layoutManager = StaggeredGridLayoutManager(3, RecyclerView.VERTICAL) countryRv.adapter = adapter } fun initViewModel() { activity.let { viewModel = ViewModelProvider( requireActivity(), viewModelFactory ).get(CountrysViewModel::class.java) viewModel.fetchCountrys().observe(this, Observer { it.forEach { Log.d("nnnn", "" + it.name) } adapter.submitList(it) }) adapter.setUpListener(object : CountrysAdapter.ItemCLickedListener { override fun onItemClicked(countryDetails: CountryDetails) { val bundle = bundleOf( "countryName" to countryDetails.name, "countryFlag" to countryDetails.flag, "timeZone" to countryDetails.timezones ) viewModel.setTranslations(countryDetails.translations) viewModel.setLanguages(countryDetails.language) navController?.navigate( R.id.action_countrysFragment_to_detailsFragment, bundle ) Log.d("cor", "" + countryDetails.name)}})}}}
In the above class, take note of the bundleOf() method where I mapped the countrys name, the countrys flag and the timezone to the respective keys which will be used later to retrieve them in the DetailsFragment,the bundle is passed as a second parameter to the navigate method to carry the data with it while moving to its destination.Also take note of the setTranslations and setLanguages method from the CountrysViewModelclass which is used to set the translations and languages of each countrys and will later be observed from TranslationsFragment and LanguagesFragment respectively.
fragment_details.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" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.details.DetailsFragment"> <ImageView android:id="@+id/flag_img" android:layout_width="match_parent" android:layout_height="200dp" android:layout_marginTop="8dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/country_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:text="country" android:textColor="@color/worldColor" android:textSize="24sp" android:textStyle="bold" app:layout_constraintEnd_toEndOf="@id/flag_img" app:layout_constraintStart_toStartOf="@id/flag_img" app:layout_constraintTop_toBottomOf="@id/flag_img" /> <TextView android:id="@+id/timezone_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:text="TimeZone" android:textColor="@color/worldColor" app:layout_constraintEnd_toStartOf="@id/timeZone" app:layout_constraintStart_toStartOf="@id/flag_img" app:layout_constraintTop_toBottomOf="@id/country_text" /> <TextView android:id="@+id/timeZone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:text="UTC" android:textColor="@color/worldColor" app:layout_constraintEnd_toEndOf="@id/flag_img" app:layout_constraintStart_toEndOf="@id/timezone_text" app:layout_constraintTop_toBottomOf="@id/country_text" /> <com.google.android.material.tabs.TabLayout android:id="@+id/tabLayout" android:layout_width="match_parent" android:layout_height="40dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/timeZone" app:tabBackground="@color/worldColor" app:tabIndicatorColor="@android:color/white" app:tabTextColor="@android:color/white" /> <androidx.viewpager2.widget.ViewPager2 android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tabLayout" /> </androidx.constraintlayout.widget.ConstraintLayout>
ViewPagerAdapter.kt
package kulloveth.developer.com.countrydetails.ui.details import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter import kulloveth.developer.com.countrydetails.ui.details.languages.LanguagesFragment class ViewPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { override fun getItemCount(): Int = 2 override fun createFragment(position: Int): Fragment = when (position) { 0 -> TranslationsFragment() 1 -> LanguagesFragment() else -> throw IllegalStateException("Invalid adapter position") }}
The above class is the adapter class for ViewPager2 present in the DetailsFragment which contains two other fragments.
DetailsFragment.kt
package kulloveth.developer.com.countrydetails.ui.details import android.app.Activity import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import com.ahmadrosid.svgloader.SvgLoader import com.google.android.material.tabs.TabLayoutMediator import kotlinx.android.synthetic.main.fragment_details.* import kulloveth.developer.com.countrydetails.R class DetailsFragment : Fragment() { private lateinit var viewPagerAdapter: ViewPagerAdapter var countryName: String? = null private var countryFlag: String? = null var timeZon: ArrayList<String>? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) countryName = arguments?.getString("countryName") countryFlag = arguments?.getString("countryFlag") timeZon = arguments?.getStringArrayList("timeZone") } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_details, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) Log.d("corn", "" + countryName) country_text.text = countryName timeZon?.forEach { timeZone.text = it } SvgLoader.pluck() .with(context as Activity?) .setPlaceHolder(R.mipmap.ic_launcher, R.mipmap.ic_launcher) .load(countryFlag, flag_img) viewPagerAdapter = ViewPagerAdapter(this) pager.adapter = viewPagerAdapter TabLayoutMediator(tabLayout, pager) { tab, position -> when (position) { 0 -> tab.text = "Translation" 1 -> tab.text = "Languages" }}.attach() }}
In the code above observe the onCreate() method where the data is been received by passing its type of data with its key to arguments. countrys name and flag are both strings so getString method is used, timezone is a list so getStringArrayList is used.
fragment_translations.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" tools:context=".ui.details.TranslationsFragment"> <TextView android:id="@+id/translation_tv" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:gravity="center" android:text="Translations" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/german_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="German" app:layout_constraintEnd_toStartOf="@id/germanTrans" app:layout_constraintStart_toStartOf="@id/translation_tv" app:layout_constraintTop_toBottomOf="@id/translation_tv" /> <TextView android:id="@+id/germanTrans" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="GermanTrans" app:layout_constraintEnd_toEndOf="@id/translation_tv" app:layout_constraintStart_toEndOf="@id/german_tv" app:layout_constraintTop_toBottomOf="@id/translation_tv" /> <TextView android:id="@+id/spanish_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Spanish" app:layout_constraintEnd_toStartOf="@id/spanishTrans" app:layout_constraintStart_toStartOf="@id/translation_tv" app:layout_constraintTop_toBottomOf="@id/german_tv" /> <TextView android:id="@+id/spanishTrans" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="SpanishTrans" app:layout_constraintEnd_toEndOf="@id/translation_tv" app:layout_constraintStart_toEndOf="@id/spanish_tv" app:layout_constraintTop_toBottomOf="@id/german_tv" /> <TextView android:id="@+id/french_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="French" app:layout_constraintEnd_toStartOf="@id/frenchTrans" app:layout_constraintStart_toStartOf="@id/translation_tv" app:layout_constraintTop_toBottomOf="@id/spanish_tv" /> <TextView android:id="@+id/frenchTrans" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="FrenchTrans" app:layout_constraintEnd_toEndOf="@id/translation_tv" app:layout_constraintStart_toEndOf="@id/french_tv" app:layout_constraintTop_toBottomOf="@id/spanish_tv" /> <TextView android:id="@+id/japan_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Japan" app:layout_constraintEnd_toStartOf="@id/japanTrans" app:layout_constraintStart_toStartOf="@id/translation_tv" app:layout_constraintTop_toBottomOf="@id/french_tv" /> <TextView android:id="@+id/japanTrans" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="JapanTrans" app:layout_constraintEnd_toEndOf="@id/translation_tv" app:layout_constraintStart_toEndOf="@id/japan_tv" app:layout_constraintTop_toBottomOf="@id/french_tv" /> <TextView android:id="@+id/italian_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Italian" app:layout_constraintEnd_toStartOf="@id/italianTrans" app:layout_constraintStart_toStartOf="@id/translation_tv" app:layout_constraintTop_toBottomOf="@id/japan_tv" /> <TextView android:id="@+id/italianTrans" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="ItalianTrans" app:layout_constraintEnd_toEndOf="@id/translation_tv" app:layout_constraintStart_toEndOf="@id/italian_tv" app:layout_constraintTop_toBottomOf="@id/japan_tv" /> <TextView android:id="@+id/persian_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Persian" app:layout_constraintEnd_toStartOf="@id/persianTrans" app:layout_constraintStart_toStartOf="@id/translation_tv" app:layout_constraintTop_toBottomOf="@id/italian_tv" /> <TextView android:id="@+id/persianTrans" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="PersianTrans" app:layout_constraintEnd_toEndOf="@id/translation_tv" app:layout_constraintStart_toEndOf="@id/persian_tv" app:layout_constraintTop_toBottomOf="@id/italian_tv" /> </androidx.constraintlayout.widget.ConstraintLayout>
TranslationsFragment.kt
package kulloveth.developer.com.countrydetails.ui.details 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.lifecycle.Observer import kotlinx.android.synthetic.main.fragment_translations.* import kulloveth.developer.com.countrydetails.R import kulloveth.developer.com.countrydetails.ui.countrys.CountrysViewModel class TranslationsFragment : Fragment() { private val viewModel: CountrysViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_translations, container,false)} override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.translationsLiveData.observe(viewLifecycleOwner, Observer { germanTrans.text = it.de spanishTrans.text = it.es frenchTrans.text = it.fr japanTrans.text = it.ja italianTrans.text = it.it persianTrans.text = it.fa})}}
TranslationsFragment is one of the fragments on the viewpager, the translations which was created in CountrysViewModel.kt and setup in CountrysFragment.kt is now been observed in this fragment with the code viewmodel.translationsLiveData.observe{}.
fragment_languages.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.details.languages.LanguagesFragment"> <androidx.recyclerview.widget.RecyclerView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/recyclerView"/> </FrameLayout>
languages_item.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/name_tv" app:layout_constraintTop_toTopOf="parent" android:layout_marginTop="8dp" app:layout_constraintEnd_toStartOf="@id/name" app:layout_constraintStart_toStartOf="parent" android:text="Name" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/name" app:layout_constraintTop_toTopOf="parent" android:layout_margin="8dp" app:layout_constraintStart_toEndOf="@id/name_tv" android:text="@string/hello_blank_fragment" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/nativeName_tv" android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="@id/name_tv" app:layout_constraintStart_toStartOf="parent" android:text="Native name:" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/nativeName" android:layout_margin="8dp" app:layout_constraintTop_toBottomOf="@id/name" app:layout_constraintStart_toEndOf="@id/nativeName_tv" android:text="@string/hello_blank_fragment" /> </androidx.constraintlayout.widget.ConstraintLayout>
LanguageAdapter.kt
package kulloveth.developer.com.countrydetails.ui.details.languages import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.languages_item.view.* import kulloveth.developer.com.countrydetails.R import kulloveth.developer.com.countrydetails.data.model.Language class LanguageAdapter : ListAdapter<Language, LanguageAdapter.MainViewHolder>( DiffCallback()) { class DiffCallback : DiffUtil.ItemCallback<Language>() { override fun areItemsTheSame(oldItem: Language, newItem: Language): Boolean { return oldItem.id == newItem.id} override fun areContentsTheSame(oldItem: Language, newItem: Language): Boolean { return oldItem.id == newItem.id}} override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {val view = LayoutInflater.from(parent.context).inflate(R.layout.languages_item, parent, false) return MainViewHolder( view)} override fun onBindViewHolder(holder: MainViewHolder, position: Int) { holder.bind(getItem(position))} class MainViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(language: Language) { itemView.name.text = language.name itemView.nativeName.text = language.nativeName }}}
The LanguagesFragment uses a recyclerview and the above is its adapter
LanguagesFragment.kt
package kulloveth.developer.com.countrydetails.ui.details.languages import android.os.Bundle import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.fragment_languages.* import kulloveth.developer.com.countrydetails.R import kulloveth.developer.com.countrydetails.ui.countrys.CountrysViewModel class LanguagesFragment : Fragment() { val adapter = LanguageAdapter() val viewModel: CountrysViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_languages, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) recyclerView.layoutManager = LinearLayoutManager(requireActivity()) recyclerView.adapter = adapter recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) activity.let { viewModel.languageLiveData.observe(viewLifecycleOwner, Observer { adapter.submitList(it) })}}}
In the above code the languageLiveData is also been observed to access the language list which was also setup in the CountrysFragment.
Also note that in each of these fragment that is receiving the data using viewmodel, the viewmodel class was initialized using activityViewModels() this is to ensure that these fragments are retrieving the activity that contains them.
Summary
In this post we talked about navigation component, how you can pass data with Bundle between fragments when using the component and also how you can pass data between fragments using viewmodel. I hope this helps you out in solving related problems, you can drop your feedback and questions in the comment section LOVETH
The github link to the full code is
kulloveth / CountryDetails
List of Countries and all possible information about them
CountryDetails
This project is based on an Api from restcountries.eu to get List of Countries and all possible information about them
IDE-Integrated Development Environment
- Android Studio- Find link on how you can setup Android studio here
Top comments (2)
Nice article.. clear explanation of code. Have one doubt. in MVVM architecture, can fragments have the responsibility to directly navigate to other fragment, or should the hosting activity take that responsibility of communication between fragments?
sorry for the late reply, navigation and communication are two different things so I am assuming you mean communication. if you decide to use viewModel for communication between two fragments then they must be attached to same activity but you can as well use Bundle as an alternative when viewModel is not possible