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