I am using nested navigation graphs in order to scope and share my viewmodels across a set of fragments.
I also have a BaseFragment class which obtains the reference to the required viewmodel:
fun provideViewModel() : VM {
return if(viewModelScopeGraphId != null) {
ViewModelProvider(findNavController().getViewModelStoreOwner(viewModelScopeGraphId!!)).get(viewModelClass)
} else {
ViewModelProvider(this).get(viewModelClass)
}
}
I can override a property, viewModelScopeGraphId, if I need the viewmodel to be scoped to the navigation graph (or nested navigation graph) with that specific id.
Ideally I would just want to set a boolean flag like useScopedViewModel and obtain the id of the current navigation graph, for example:
fun provideViewModel() : VM {
return if(useScopedViewModel) {
ViewModelProvider(findNavController().getViewModelStoreOwner(getCurrentNavGraphId())).get(viewModelClass)
} else {
ViewModelProvider(this).get(viewModelClass)
}
}
I have tried using navController.graph.id to get the current graph id, but it seems the id I get from there does not match up with my resource id's (eg. R.id.nav_graph). Is there something I am missing?
I have a similar issue (more context of my case at the end), tried a few solutions, none successful.
In the end, I give up and just give the graph id as a parameter for the Fragment.
As it could help you to achieve your own solution, so my solution looks like the following:
Have to create this extension, as the original navGraphViewModels only accepts a #IdRes navGraphId: Int and I want to lazy load the arguments, etc. (Also I've simplified removing the factoryProducer from the arguments, as so far I will not use it for our solution.
inline fun <reified VM : ViewModel> Fragment.navGraphViewModels(
noinline graphIdProducer: () -> Int
): Lazy<VM> {
val backStackEntry by lazy {
findNavController().getBackStackEntry(graphIdProducer())
}
val storeProducer: () -> ViewModelStore = {
backStackEntry.viewModelStore
}
return createViewModelLazy(VM::class, storeProducer, {
backStackEntry.defaultViewModelProviderFactory
})
}
In my fragment where I want to use it, I recover the Arguments using the navArgs, and recover the ViewModel using the extension above:
private val listenerViewModel: ListenerViewModel by navGraphViewModels {
navArgs.graphId
}
private val navArgs: MyFragmentArgs by navArgs()
And to whoever needs to "listen" for that ViewModel, can simply load using the Navigation navGraphViewModels:
private val listenerViewModel: ListenerViewModel by navGraphViewModels(
R.id.my_graph_a
)
And from another graph/fragment I simple do:
private val listenerViewModel: ListenerViewModel by navGraphViewModels(
R.id.my_graph_b
)
To explain a little the context of my case:
I have two distinct Fragments, where the user can click in a Country selection.
Each of these Fragments is inside a different Graph, as they are different flows.
The Country selection is a Fragment where load the supported countries list from an API, displays it, pre-select any previous user selection (also given as a FragmentArgs), and the user can change the selection, which implies coming back to the previous screen, with the newly selected value or just come back not triggering anything here.
I'm aware and expecting the release of https://issuetracker.google.com/issues/79672220, but as it is today (March-2020) it is only available in alpha.
You may try to check it with graph's start destination.
when (navController.graph.startDestinationId) {
R.id.firstFragmentOfFirstGraph -> { /* First graph */ }
R.id.firstFragmentOfSecondGraph -> { /* Second graph */}
}
Related
I've seen some Jetpack Compose projects and I've seen two types of managing states, not realizing which one is better.
For example, let's assume: the input state. I've seen people manage this state in the UI, using remember to save the state of the value.
Another way I've seen is to create this mutableState in the ViewModel and store/use it from there. What's the best way to do this?
In addition to #Thracian's answer.
Let me share my thought process based on my current level of experience in Jetpack Compose. Just a disclaimer, I'm still in the learning curve.
IMO, theres no such thing as "best", things in our field evolves, what might be considered "best" today may become obsolete tomorrow, but there are certain practices that are "recommended", approved and adopted by the community which might save you from dealing with some pitfalls (e.g unwanted re-compositions, infinite navhost calls( you already dealt with this) etc..), but its up to you if you will follow it or not.
So what your'e trying to understand is called State Hoisting. The way I could explain this is by just simply sampling a scenario (again this is based on my own experience with how I apply my knowledge in Jetpack Compose).
Consider a Login use-case with 3 different levels of complexity
A Login UI prototype : — Just showcasing your potential Login Screen design and user interaction
Login UI Mock-up : — With a bit of validation and some toast showing a negative scenario, just an advance version of the prototype
A fully working Login module — where you have to construct view models, bind things to lifecycles, perform concurrent operations etc..
At this point, you already have an idea the different levels of state management based on the use-case above.
For a Login prototype, I won't be needing a state class or a view model, since its just a prototype
#Composable
fun LoginScreen() {
val userName by remember { <mutable string state username> }
val password by remember { <mutable string state password> }
Column {
Text(text = username)
Text(text = password)
Button("Login")
}
}
and because its a very simple UI(composable), I only need to specify basic structure of a composable using remember + state, showcasing an input is happening.
For the Login mock-up with simple validation, we utilized the recommended state hoisting using a class,
class LoginState {
var event;
var mutableUserNameState;
var mutablePasswordState;
fun onUserNameInput() {...}
fun onPasswordInput() {...}
fun onValidate() {
if (not valid) {
event.emit(ShowToast("Not Valid"))
} else {
event.emit(ShowToast("Valid"))
}
}
}
#Composable
fun LoginScreen() {
val loginState by remember { LoginState }
LaunchedEffect() {
event.observe {
it.ShowToast()
}
}
Column {
Text(text = loginState.mutableUserNameState, onInput = { loginState.onUserNameInput()} )
Text(text = loginState.mutablePasswordState, onInput = { loginState.onPasswordInput()} )
Button(loginState.onValidate)
}
}
Now for a full blown Login Module, where your'e also taking lifecylce scopes into consideration
class LoginViewModel(
val userRepository: UserRepository // injected by your D.I framework
): ViewModel {
var event;
var mutableUserNameState;
var mutablePasswordState;
fun onUserNameInput() {...}
fun onPasswordInput() {...}
fun onValidateViaNetwork() {
// do a non-blocking call to a server
viewModelScope.launch {
var isUserValid = userRepository.validate(username, password)
if (isUserValid) {
event.emit(ShowToast("Valid"))
} else {
event.emit(ShowToast("Not Valid"))
}
}
}
}
#Composable
fun LoginScreen() {
val userNameState by viewModel.mutableUserNameState
val passwordState by viewModel.mutablePasswordState
LaunchedEffect() {
event.observe {
it.ShowToast()
}
}
Column {
Text(text = userNameState, onInput = { viewModel.onUserNameInput()} )
Text(text = passwordState, onInput = { viewModel.onPasswordInput()} )
Button(viewModel.onValidateViaNetwork)
}
}
Again, this is just based on my experience and how I decide on hoisting my states. As for the snippets I included, I tried to make them as pseudo as possible without making them look out of context so they are not compilable. Also mock and prototype are considered the same, I just used them in conjunction to put things into context.
It depends on your preference. Using states inside a Composable if you are building a standalone Composable or a library is preferred. Any class you see with rememberXState() keeps state variable. For instance scrollState()
#Composable
fun rememberScrollState(initial: Int = 0): ScrollState {
return rememberSaveable(saver = ScrollState.Saver) {
ScrollState(initial = initial)
}
}
#Stable
class ScrollState(initial: Int) : ScrollableState {
/**
* current scroll position value in pixels
*/
var value: Int by mutableStateOf(initial, structuralEqualityPolicy())
private set
// rest of the code
}
This is a common approach in Jetpack Compose. I use this approach in libraries i build, for instance in this image crop library, i keep state and Animatable. Animatable which is low level default animation class also has hold its own states.
#Suppress("NotCloseable")
class Animatable<T, V : AnimationVector>(
initialValue: T,
val typeConverter: TwoWayConverter<T, V>,
private val visibilityThreshold: T? = null
) {
internal val internalState = AnimationState(
typeConverter = typeConverter,
initialValue = initialValue
)
/**
* Current value of the animation.
*/
val value: T
get() = internalState.value
/**
* Velocity vector of the animation (in the form of [AnimationVector].
*/
val velocityVector: V
get() = internalState.velocityVector
/**
* Returns the velocity, converted from [velocityVector].
*/
val velocity: T
get() = typeConverter.convertFromVector(velocityVector)
/**
* Indicates whether the animation is running.
*/
var isRunning: Boolean by mutableStateOf(false)
private set
/**
* The target of the current animation. If the animation finishes un-interrupted, it will
* reach this target value.
*/
var targetValue: T by mutableStateOf(initialValue)
private set
}
and so on. This approach is doing for ui components that don't involve business logic but Ui logic.
When you need to update your Ui based on business logic like search or getting results from an API you should use a Presenter class which can be ViewModel too.
Last but least people are now questioning whether there should be a ViewModel with Jetpack Compose since we can use states with an AAC ViewModel. And cashapp introduced molecule library, you can check it out either.
Also this link about state holders is good source to read
I know this is a very documented topic, but I couldn't find a way to implement it in my project, even after spending hours trying to figure it out.
My root problem is that I have a RecyclerView with an Adapter whose content isn't updating as I'd like. I'm a beginner in Android, so I didn't implement any MVVM or such architecture, and my project only contains a repository, fetching data from Firebase Database, and passing it to a list of ShowModel, a copy of said list being used in my Adapter to display my shows (In order to filter/sort them without modifying the list with all shows).
However, when adding a show to the database from another Activity, my Adapter isn't displaying the newly added show (as detailed here)
I was told to use LiveData and ViewModel, but even though I started understanding how it works after spending time researching it, I don't fully get how I should use it in order to implement it in my project.
Currently I have the following classes:
The Adapter:
class ShowAdapter(private val context: MainActivity, private val layoutId: Int, private val textNoResult: TextView?) : RecyclerView.Adapter<ShowAdapter.ViewHolder>(), Filterable {
var displayList = ArrayList(showList)
class ViewHolder(view : View) : RecyclerView.ViewHolder(view){
val showName: TextView = view.findViewById(R.id.show_name)
val showMenuIcon: ImageView = view.findViewById(R.id.menu_icon)
}
#SuppressLint("NewApi")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
return ViewHolder(view)
}
#SuppressLint("NewApi", "WeekBasedYear")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentShow = displayList[position]
val index = holder.adapterPosition
holder.showName.text = currentShow.name
holder.itemView.setOnClickListener{ // Display show content
val intent = Intent(context, DetailsActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
}
holder.showMenuIcon.setOnClickListener{
val popupMenu = PopupMenu(context, it)
popupMenu.menuInflater.inflate(R.menu.show_management_menu, popupMenu.menu)
popupMenu.show()
popupMenu.setOnMenuItemClickListener {
when(it.itemId){
R.id.edit -> { // Edit show
val intent = Intent(context, AddShowActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
return#setOnMenuItemClickListener true
}
R.id.delete -> { // Delete show
val repo = ShowRepository()
repo.deleteShow(currentShow)
displayList.remove(currentShow)
notifyItemRemoved(index)
return#setOnMenuItemClickListener true
}
else -> false
}
}
}
}
override fun getItemCount(): Int = displayList.size
// Sorting/Filtering methods
}
The fragment displaying the adapter:
class HomeFragment : Fragment() {
private lateinit var context: MainActivity
private lateinit var verticalRecyclerView: RecyclerView
private lateinit var buttonAddShow: Button
private lateinit var showsAdapter: ShowAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
context = getContext() as MainActivity
buttonAddShow = view.findViewById(R.id.home_button_add_show)
buttonAddShow.setOnClickListener{ // Starts activity to add a show
startActivity(Intent(context, AddShowActivity::class.java))
}
verticalRecyclerView = view.findViewById(R.id.home_recycler_view)
showsAdapter = ShowAdapter(context, R.layout.item_show, null)
verticalRecyclerView.adapter = showsAdapter
return view
}
}
The MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadFragment(HomeFragment())
}
private fun loadFragment(fragment: Fragment){
val repo = ShowRepository()
if(showsListener != null) databaseRef.removeEventListener(showsListener!!)
repo.updateData{
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.addToBackStack(null)
if(supportFragmentManager.isStateSaved)transaction.commitAllowingStateLoss()
else transaction.commit()
}
}
}
The repository:
class ShowRepository {
object Singleton{
val databaseRef = FirebaseDatabase.getInstance().getReference("shows")
val showList = arrayListOf<ShowModel>()
var showsListener: ValueEventListener? = null
}
fun updateData(callback: () -> Unit){
showsListener = databaseRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
showList.clear()
for(ds in snapshot.children){
val show = ds.getValue(ShowModel::class.java)
if(show != null) showList.add(show)
}
callback()
}
override fun onCancelled(p0: DatabaseError) { }
})
}
fun insertShow(show: ShowModel){
databaseRef.child(show.id).setValue(show)
}
fun deleteShow(show: ShowModel){
databaseRef.child(show.id).removeValue()
}
}
From what I understand of LiveData and ViewModel, what I should do is creating a ShowViewModel containing a MutableLiveData<List<ShowModel>> containing the shows, and then observe it in my HomeFragment and update the adapter depending on the changes happening. However, everytime I start something to implement it, I encounter a situation where I'm lost and don't know what I should do, which leads me back to square one once again. I've been trying this for more than a week without progressing even a little bit, and that's why I'm here, hoping for some insight.
Sorry for the silly question and the absurd amount of informations, and hoping someone will be able to help me understand what I do wrong/should do.
(this ended up longer than I meant it to be - hope it's not too much! There's a lot to learn, but you don't have to make it super complicated at first)
Broadly, working backwards, it should go like this:
Adapter
displays whatever the Fragment tells it to (some kind of setData function that updates its internal list and refreshes)
passes events to the Fragment (deleteItem(item), showDetails(item) etc.) - don't have the Adapter doing things like starting Activites, that's not its responsibility
Fragment
grabs a reference to any ViewModels (only certain components like Fragments and Activities can actually "own" them)
observes any LiveData (or collects Flows if you're doing it that way) on the VM, and updates stuff in the UI in response
e.g. model.shows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
handles UI events and calls methods on the VM in response, e.g. click listeners, events from the Adapter
ViewModel
acts as a go-between for the UI (the Fragment) and the data layer (the repository)
exposes methods for handling events like deleting items, interacts with the data layer as required (e.g. calling the appropriate delete function)
exposes data state for the UI to observe, so it can react to changes/updates (e.g. a LiveData containing the current list of shows that the data layer has provided)
That's the basic setup - the VM exposes data which the UI layer observes and reacts to, by displaying it. The UI layer also produces events (usually down to user interaction) which are passed to the VM. You can read more about this general approach in this guide about app architecture - it's worth reading because not only is it recommended as a way to build apps, a lot of the components you use in modern Android are designed with this kind of approach in mind (like the reactive model of wiring stuff up).
You could handle the Adapter events like this:
// in your Adapter
var itemDeletedListener: ((Item) -> Unit)? = null
// when the delete event happens for an item
itemDeletedListener?.invoke(item)
// in your Fragment
adapter.itemDeletedListener = { viewModel.deleteItem(it) }
which is easier than implementing an interface, and lets you wire up your Adapter similar to doing setOnClickListener on a button. Notice we're passing the actual Item object here instead of a list index - generally this is easier to work with, you don't need to maintain multiple copies of a list just so you can look up an index given to you by something else. Passing a unique ID can make sense though, especially if you're working with a database! But usually the object itself is more useful and consistent
The data layer is the tricky bit - the ViewModel needs to communicate with that to get the current state. Say you delete an item - you then need to get the current, updated list of shows. You have three approaches:
Call the delete function, immediately after fetch the current data, and set it on the appropriate LiveData
This can work, but it's not very reactive - you're doing one action, then immediately doing another because you know your data is stale. It would be better if the new data just arrived automatically and you could react to that by pushing it out. The other issue is that calling the delete function might not have an immediate effect - if you fetch the current data, nothing might have changed yet. It's better if the data layer is responsible for announcing updates.
This is the simplest approach though, and probably a good start! You could run this task in a coroutine (viewModelScope.launch { // delete and fetch and update LiveData }) so any slowness doesn't block the current thread.
Have the data layer's functions return the current, updated data that results
Similar to above, you're just sort of pushing the fetching into the data layer. This requires all those functions to be written to return the current state, which could take a while! And depending on what data you want, this might be impossible - if you have an active query on some data, how does the function know what specific data to return?
Make the ViewModel observe the data it wants, so when the data layer updates, you get the results automatically
This is the recommended reactive approach - again it's that two-way idea. The VM calling a function on the data layer is completely separate from the VM receiving new data. One thing just happens as a natural consequence of the other, they don't need to be tied together. You just need to wire them up right!
How do you actually do that though? If you're working with something like Room, that's already baked in. Queries can return async data providers like LiveData or Flows - your VM just needs to observe those and expose the results, or just expose them directly. That way, when a table is updated, any queries (like the current shows) push a new value, and the observers receive it and do whatever they need to do, like telling the Adapter to display the data. It all Just Works once it's wired up.
Since you have your own repo, you need to expose your own data sources. You could have a currentShows LiveData or (probably preferably) the flow equivalent, StateFlow. When the repo initialises, and when any data is changed, it updates that currentShows data. Anything observing that (e.g. the VM, the Fragment through a LiveData/Flow that the VM exposes) will automatically get the new values. So broadly:
// Repo
// this setup is exactly the same as your typical LiveData, except you need an initial value
private val _currentShows = MutableStateFlow<List<Show>>(emptyList()) // or whatever default
val currentShows: StateFlow<List<Show>> = _currentShows
fun deleteItem(item: Item) {
// do the deletion
// get the updated show list
_currentShows.value = updatedShowList
}
// ViewModel
// one way of doing things - you have a lot of options! This literally just exposes
// the state from the data layer, and turns it into a LiveData (if you want that)
val currentShows = repo.currentShows.asLiveData()
// Fragment
// wire things up so you handle new data as it arrives
viewModel.currentShows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
That's basically it. I've skimmed over a lot because honestly, there's a lot to learn with this - especially about Flows and coroutines if you're not already familiar with those. But hopefully that gives you an overview of the general idea, and don't be afraid to take shortcuts (like just updating your data in the ViewModel by setting its LiveData values) while you're learning and getting the hang of it. Definitely give that app architecture guide a read, and also the guides for ViewModels and LiveData. It'll start to click when you get the general idea!
It seems like recommended pattern for fields in viewmodel is:
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
(btw, is it correct that the selected field isn't private?)
But what if I don't need to subscribe to the changes in the ViewModel's field. I just need passively pull that value in another fragment.
My project details:
one activity and a bunch of simple fragments replacing each other with the navigation component
ViewModel does the business logic and carries some values from one fragment to another
there is one ViewModel for the activity and the fragments, don't see the point to have more than one ViewModel, as it's the same business flow
I'd prefer to store a value in one fragment and access it in the next one which replaces the current one instead of pass it into a bundle and retrieve again and again manually in each fragment
ViewModel:
private var amount = 0
fun setAmount(value: Int) { amount = value}
fun getAmount() = amount
Fragment1:
bnd.button10.setOnClickListener { viewModel.setAmount(10) }
Fragment2:
if(viewModel.getAmount() < 20) { bnd.textView.text = "less than 20" }
Is this would be a valid approach? Or there is a better one? Or should I just use LiveData or Flow?
Maybe I should use SavedStateHandle? Is it injectable in ViewModel?
To answer your question,
No, It is not mandatory to use LiveData always inside ViewModel, it is just an observable pattern to inform the caller about updates in data.
If you have something which won't be changed frequently and can be accessed by its instance. You can completely ignore wrapping it inside LiveData.
And anyways ViewModel instance will be preserved and so are values inside it.
And regarding private field, MutableLiveData should never be exposed outside the class, as the data flow is always from VM -> View which is beauty of MVVM pattern
private val selected = MutableLiveData<Item>()
val selectedLiveData : LiveData<Item>
get() = selected
fun select(item: Item) {
selected.value = item
}
I am having difficulty with implementing a proper navigation structure in my app. I want it to behave similarly to the navigation in YouTube and Instagram. The biggest problem I am having is with the backstack and fragment recreation.
I'm currently using the single activity with multiple fragments approach. I have an app bar and bottom navigation view with 3 menu items setup in the main activity. The app bar has one menu item that navigates to the user profile fragment when selected, and each of the bottom nav's menu items navigates to different root fragments(home, search, and profile) when selected. I'm also using google's firebase database and firestore to store user data(email, uid, password, etc...) and photos.
I've tried using the supportFragmentManager.beginTransaction().replace way, and android jetpack's navigation architecture, but haven't been able to produce the results I need with either.
I'm able to navigate to the proper destinations using the supportFragmentManager way, but can't seem to implement a proper backwards navigation structure. I've tried to find other code samples of implementing this, but was unable to find anything that works, and a lot of these samples are older versions in java code with deprecated methods, which makes it difficult when trying to convert to kotlin code.
The jetpack navigation component is a bit easier to use, but I cannot get it to behave properly either. To my knowledge, the current navigation does not support multiple backstacks and does not have a proper backwards navigation structure unless you add the NavigationExtensions file provided here: https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample. Using this sample, I am having problems with:
1.Navigating backwards does not return to the originally saved fragment state, it instead recreates a brand new fragment.
2.Navigating to the profile fragment from the app bar works, but crashes when the user is inside the fragment and presses it again.
3.Passing a default set of arguments to the user fragment item menu in the bottom navigation view. I originally had the account profile fragment tied to a bottom nav menu item (still do for testing purposes) with the logged in user's uid set as the default arguments. The fragment(UserFragment) used takes the uid argument and uses it to fetch the proper information from google's firebase. I was previously able to achieve this by using the regular jetpack navigation component(without the advanced sample) and adding the following code in the MainActivity:
val navArgument1 = NavArgument.Builder().setDefaultValue(uid).build()
val orderDestination = navController.graph.findNode(R.id.user_Fragment)
orderDestination?.addArgument("destinationUid",navArgument1)
Then within the user fragment, I use this code to get the proper uid:
uid = arguments?.getString("destinationUid")
With the advanced sample navigation component, I'm not able to pass this default argument into the user fragment. I keep getting an error that says something like "There is no navigation controller associated with this fragment," and the app crashes.
The Main Activity
class ExploreActivity : AppCompatActivity(),BottomNavigationView.OnNavigationItemSelectedListener{
override fun onNavigationItemSelected(p0:MenuItem):Boolean{
when(p0.itemId){
R.id.home->{
val homeViewFragment = HomeViewFragment()
supportFragmentManager.beginTransaction().replace(R.id.nav_host_fragment,homeViewFragment).commit()
return true
}
R.id.world->{
val publicViewFragment = PublicViewFragment()
supportFragmentManager.beginTransaction().replace(R.id.nav_host_fragment,publicViewFragment).commit()
return true
}
R.id.account->{
val userFragment = UserFragment()
val bundle = Bundle()
val uid=FirebaseAuth.getInstance().currentUser?.uid
bundle.putString("destinationUid",uid)
userFragment.arguments=bundle
supportFragmentManager.beginTransaction().replace(R.id.nav_host_fragment,userFragment).commit()
return true
}
}
return false
}
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_explore)
bottom_navigation_explore.setOnNavigationItemSelectedListener(this)
bottom_navigation_explore.selectedItemId=R.id.home
}
override fun onActivityResult(requestCode:Int,resultCode:Int,data:Intent?){
super.onActivityResult(requestCode,resultCode,data)
if(requestCode==UserFragment.PICK__PROFILE_FROM_ALBUM&&resultCode==Activity.RESULT_OK){
val imageUri=data?.data
val uid=FirebaseAuth.getInstance().currentUser?.uid
val storageRef=FirebaseStorage.getInstance().reference.child("userProfileImages")
.child(uid!!)
storageRef.putFile(imageUri!!).continueWithTask{task:Task<UploadTask.TaskSnapshot>->
return#continueWithTask storageRef.downloadUrl
}.addOnSuccessListener{uri->
val map=HashMap<String,Any>()
map["image"]=uri.toString()
FirebaseFirestore.getInstance().collection("profileImages").document(uid).set(map)
}
}
}
}
User Fragment
class UserFragment : Fragment(){
var fragmentView : View? = null
var firestore : FirebaseFirestore? = null
var uid : String? = null
var auth : FirebaseAuth? = null
var currentUserUid : String? = null
companion object{
var PICK__PROFILE_FROM_ALBUM = 10
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
fragmentView = LayoutInflater.from(activity).inflate(R.layout.activity_main,container,false)
uid = arguments?.getString("destinationUid")
firestore = FirebaseFirestore.getInstance()
auth = FirebaseAuth.getInstance()
currentUserUid = auth?.currentUser?.uid
return fragmentView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if(uid == currentUserUid) {
fragmentView?.btn_follow_signout_main?.text = "Signout"
fragmentView?.btn_follow_signout_main?.setOnClickListener {
activity?.finish()
startActivity(Intent(activity, LoginActivity::class.java))
auth?.signOut()
}
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),1)
iv_createpost_main.setOnClickListener {
if (ContextCompat.checkSelfPermission(context!!, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED){
startActivity(Intent(activity,CreatePost::class.java))
}
return#setOnClickListener
}
//add explanation of why permission is needed
if (ContextCompat.checkSelfPermission(context!!, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
iv_profilepicture_main.setOnClickListener {
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
activity?.startActivityForResult(intent, PICK__PROFILE_FROM_ALBUM)
}
}
}
else{
fragmentView?.btn_follow_signout_main?.text = "Follow +"
fragmentView?.btn_follow_signout_main?.setOnClickListener {
requestFollow()
}
}
getProfileImage()
getUserName()
}
private fun getProfileImage() {
firestore?.collection("profileImages")!!.document(uid!!).get().addOnCompleteListener { task ->
if(task.isSuccessful){
val url = task.result!!["image"]
if(url != null){
Glide.with(activity!!).load(url).into(iv_profilepicture_main)
}
else{
iv_profilepicture_main.setImageResource(R.drawable.ic_account)
}
}
}
}
private fun getUserName(){
firestore?.collection("users")!!.document(uid!!).get().addOnCompleteListener { task ->
if(task.isSuccessful){
val username = task.result!!["username"]
if(username != null){
activity?.setTitle("" + username)
}
}
}
}
}
My project is currently setup using the support fragment manager, but I've been going back and forth between using it and the navigation component to try and make things work.
I have two other fragments tied to the bottom nav, but I've only included the relevant code where I believe my problem lies. The other two fragments have a user profile picture that navigates the user to the selected profile when it is clicked. I do not have any problems with those transactions, because I can easily apply the bundle and arguments with the setOnClickListener method.
TL;DR
To summarize everything: I am looking for a way to implement a proper navigation flow throughout my app. I'm having problems with backwards navigation and fragments being recreated when they shouldn't. I've tried using the fragment manager and the android jetpack navigation component, but haven't had luck with either. If anyone has any information on how to achieve this using android kotlin and the latest methods, and would like to share, I'd appreciate it.
Thanks.
So far I'm successfully able to navigate to dialogs and back using only navigation component. The problem is that, I have to do some stuff in dialog and return result to the fragment where dialog was called from.
One way is to use shared viewmodel. But for that I have to use .of(activity) which leaves my app with a singleton taking up memory, even when I no longer need it.
Another way is to override show(fragmentManager, id) method, get access to fragment manager and from it, access to previous fragment, which could then be set as targetfragment. I've used targetFragment approach before where I would implement a callback interface, so my dialog could notify targetFragment about result. But in navigation component approach it feels hacky and might stop working at one point or another.
Any other ways to do what I want? Maybe there's a way to fix issue on first approach?
Thanks to #NataTse and also the official docs, i came up with the extensions so that hopefully less boilerplate code to write:
fun <T>Fragment.setNavigationResult(key: String, value: T) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(
key,
value
)
}
fun <T>Fragment.getNavigationResult(#IdRes id: Int, key: String, onResult: (result: T) -> Unit) {
val navBackStackEntry = findNavController().getBackStackEntry(id)
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains(key)
) {
val result = navBackStackEntry.savedStateHandle.get<T>(key)
result?.let(onResult)
navBackStackEntry.savedStateHandle.remove<T>(key)
}
}
navBackStackEntry.lifecycle.addObserver(observer)
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}
In Navigation 2.3.0-alpha02 and higher, NavBackStackEntry gives access to a SavedStateHandle. A SavedStateHandle is a key-value map that can be used to store and retrieve data. These values persist through process death, including configuration changes, and remain available through the same object. By using the given SavedStateHandle, you can access and pass data between destinations. This is especially useful as a mechanism to get data back from a destination after it is popped off the stack.
To pass data back to Destination A from Destination B, first set up Destination A to listen for a result on its SavedStateHandle. To do so, retrieve the NavBackStackEntry by using the getCurrentBackStackEntry() API and then observe the LiveData provided by SavedStateHandle.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = findNavController();
// We use a String here, but any type that can be put in a Bundle is supported
navController.currentBackStackEntry?.savedStateHandle?.getLiveData("key")?.observe(
viewLifecycleOwner) { result ->
// Do something with the result.
}
}
In Destination B, you must set the result on the SavedStateHandle of Destination A by using the getPreviousBackStackEntry() API.
navController.previousBackStackEntry?.savedStateHandle?.set("key", result)
When you use Navigation Component with dialogs, this part of code looks not so good (for me it returned nothing)
navController.currentBackStackEntry?.savedStateHandle?.getLiveData("key")?.observe(
viewLifecycleOwner) { result ->
// Do something with the result.}
You need to try way from official docs and it help me a lot
This part is working for me:
val navBackStackEntry = navController.getBackStackEntry(R.id.target_fragment_id)
// Create observer and add it to the NavBackStackEntry's lifecycle
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains("key")
) {
val result =
navBackStackEntry.savedStateHandle.get<Boolean>("key")
// Do something with the result
}
}
navBackStackEntry.lifecycle.addObserver(observer)
// As addObserver() does not automatically remove the observer, we
// call removeObserver() manually when the view lifecycle is destroyed
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
And in your dialog:
navController.previousBackStackEntry?.savedStateHandle?.set(
"key",
true
)