I am integrating Compose into my app and I am confused about the use of multiple remember calls when managing state using state holders. Consider the following example:
State holder:
class MyScreenState(
val listState: LazyListState,
private val fragment: Fragment
) {
fun doSomethingWithFragment() {
...
...
}
}
Remember function:
#Composable
fun rememberMyScreenState(
listState: LazyListState = rememberLazyListState(),
fragment: Fragment
) = remember(
listState,
fragment
) {
MyScreenState(
listState = listState,
fragment = fragment
)
}
Fragment:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return ComposeView(requireContext()).apply {
setContent {
val state = rememberMyScreenState(this#MyScreenFragment)
...
}
}
}
As it is stated in documentation, each state holder should also provide its remember function which calls remember with the passed values and creates the holder.
But why are the default values of this function usually another remember functions? What additional value does it bring to the table? Why can't I simply default to to LazyListState():
#Composable
fun rememberMyScreenState(
listState: LazyListState = LazyListState(),
fragment: Fragment
)
And what should I do if I have a value that doesn't have its own remember "wrapper", like fragment from the example above. Can I safely pass it like this or do I also need to wrap it for some reason?
remember will save everything inside its block, but this does not apply to function parameters.
So this line listState: LazyListState = LazyListState() will create a new state every time it is recomposed - and the old state will be forgotten.
So all parameters should be remembered by themselves (if necessary).
Related
I'm working on an Android app using Kotlin. The app uses fragment-based navigation but I am using some Jetpack Compose to build some elements of it instead of using RecyclerViews and such.
Right now I have a card composable that builds itself off an object and another one that creates a list of those with a LazyColumn. The card has it's own separate file but the list composable is part of the code of the fragment that uses it. This is because when one of the cards is clicked, it calls a function to load a fragment that lists the details of the object the card represents (Events in this case).
This is the code in my list fragment:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_liste_evenement,container,false).apply {
val listeEvens : ArrayList<Événement> = ArrayList<Événement>()
listeEvens.add(évén)
listeEvens.add(évén2)
listeEvens.add(évén3)
val composeView = findViewById<ComposeView>(R.id.listeBlocsEven)
composeView.setContent {
ListeCarteÉvénements(événements = listeEvens)
}
}
}
#Composable
fun ListeCarteÉvénements(événements: List<Événement>) {
LazyColumn {
items(événements) { e ->
CarteÉvénement(événement = e,clickEvent = { loadFragment(details_evenement(e)) })
}
}
}
This is the card composable's declaration:
#Composable
fun CarteÉvénement(événement: Événement,clickEvent: () -> Unit) {
Column(modifier = Modifier
.clip(RectangleShape)
.padding(all = 8.dp)
.fillMaxWidth()
.height(300.dp)
.background(MaterialTheme.colors.primaryVariant)
.clickable(onClick = clickEvent))
private fun loadFragment(fragment: Fragment) {
val transaction = requireActivity().supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragmentContainerView, fragment)
transaction.addToBackStack(null)
transaction.commit()
}
As you can see, doing it this way allows me to get direct access to the event cards so that I can give my details fragment the clicked event as an attribute.
This all works but my question is: If I wanted to put the list composable in the same file as the card(outside of the fragment), how would I pass it the loadFragment function that receives a fragment that also has it's own parameter(in this case the event from the clicked card)?
You can pass a lambda callback to ListeCarteÉvénements which receives the event as an argument.
override fun onCreateView (...) : View {
...
composeView.setContent {
ListeCarteÉvénements(
événements = listeEvens,
onItemClick = { e -> loadFragment(details_evenement(e)) }
)
}
...
}
#Composable
fun ListeCarteÉvénements(événements: List<Événement>, onItemClick: (Événement) -> Unit {
LazyColumn {
items(événements) { e ->
CarteÉvénement(événement = e,clickEvent = { onItemClick(e) })
}
}
}
I am using nested recyclerview.
In the picture, the red box is the Routine Item (Parent Item), and the blue box is the Detail Item (Child Item) in the Routine Item.
You can add a parent item dynamically by clicking the ADD ROUTINE button.
Similarly, child items can be added dynamically by clicking the ADD button of the parent item.
As a result, this function works just fine.
But the problem is in the code I wrote.
I use a ViewModel to observe and update parent item addition/deletion.
However, it does not observe changes in the detail item within the parent item.
I think it's because LiveData only detects additions and deletions to the List.
So I put _items.value = _items.value code to make it observable when child items are added and deleted.
This way, I didn't even have to use update code like notifyDataSetChanged() in the child adapter.
In the end it is a success, but I don't know if this is the correct code.
Let me know if you have additional code you want!
In Fragment.kt
class WriteRoutineFragment : Fragment() {
private var _binding : FragmentWriteRoutineBinding? = null
private val binding get() = _binding!!
private lateinit var adapter : RoutineAdapter
private val vm : WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = FragmentWriteRoutineBinding.inflate(inflater, container, false)
adapter = RoutineAdapter(::addDetail, ::deleteDetail)
binding.rv.adapter = this.adapter
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
getTabPageResult()
// RecyclerView Update
vm.items.observe(viewLifecycleOwner) { updatedItems ->
adapter.setItems(updatedItems)
}
}
private fun getTabPageResult() {
val navController = findNavController()
navController.currentBackStackEntry?.also { stack ->
stack.savedStateHandle.getLiveData<String>("workout")?.observe(
viewLifecycleOwner, Observer { result ->
vm.addRoutine(result) // ADD ROUTINE
stack.savedStateHandle?.remove<String>("workout")
}
)
}
}
private fun addDetail(pos: Int) {
vm.addDetail(pos)
}
private fun deleteDetail(pos: Int) {
vm.deleteDetail(pos)
}
}
ViewModel
class WriteRoutineViewModel : ViewModel() {
private var _items: MutableLiveData<ArrayList<RoutineModel>> = MutableLiveData(arrayListOf())
val items: LiveData<ArrayList<RoutineModel>> = _items
fun addRoutine(workout: String) {
val item = RoutineModel(workout, "TEST")
_items.value?.add(item)
// _items.value = _items.value
}
fun addDetail(pos: Int) {
val detail = RoutineDetailModel("TEST", "TEST")
_items.value?.get(pos)?.addSubItem(detail) // Changing the parent item's details cannot be observed by LiveData.
_items.value = _items.value // is this right way?
}
fun deleteDetail(pos: Int) {
if(_items.value?.get(pos)?.getSubItemSize()!! > 1)
_items.value?.get(pos)?.deleteSubItem() // is this right way?
else
_items.value?.removeAt(pos)
_items.value = _items.value // is this right way?
}
}
This is pretty standard practice when using a LiveData with a mutable List type. The code looks like a smell, but it is so common that I think it's acceptable and people who understand LiveData will understand what your code is doing.
However, I much prefer using read-only Lists and immutable model objects if they will be used with RecyclerViews. It's less error prone, and it's necessary if you want to use ListAdapter, which is much better for performance than a regular Adapter. Your current code reloads the entire list into the RecyclerView every time there is any change, which can make your UI feel laggy. ListAdapter analyzes automatically on a background thread your List for which items specifically changed and only rebinds the changed items. But it requires a brand new List instance each time there is a change, so it makes sense to only use read-only Lists if you want to support using it.
I followed the compose documentation to create a bottom navigation bar by creating such a sealed class
sealed class Screen(val route: String, val label: String, val icon: ImageVector) {
object ScreenA: Screen("screenA", "ScreenA", Icons.Default.AccountBox)
object ScreenB: Screen("screenB", "ScreenB", Icons.Default.ThumbUp
}
with something such as the following screen, simply containing a Text item.
#Composable
fun ScreenA() {
Text(text = "This is ScreenA",
style = TextStyle(color = Color.Black, fontSize = 36.sp),
textAlign = TextAlign.Center)
}
and have also implemented the Scaffold, BottomNavigation, and NavHost exactly like in the docs and everything is working just fine.
Let´s say I now want to have one of the screens with a whole bunch of data displayed in a list with all sorts of business logic methods which would need a Fragment, ViewModel, Repository, and so on.
What would be the correct approach be? Can I still create the fragments and somehow pass them to the sealed class or should we forget about fragments and make everything composable?
Since you are following the documentation you need to forget about Fragments. Documentations suggests that view models per screen will be instantiated when the composable function is called/navigated to. The viewmodel will live while the composeable is not disposed. This is also motivated by single activity approach.
#Inject lateinit var factory : ViewModelProvider.Factory
#Composable
fun Profile(profileViewModel = viewModel(factory),
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit) {
}
This can be seen in compose samples here.
If you want to mix both, you can omit using navigation-compose, and roll with navigation component and use Fragment and only use composable for fragment UI.
#AndroidEntryPoint
class ProfileFragment: BaseFragment() {
private val profileViewModel by viewModels<ProfileViewModel>(viewModelFactory)
private val profileFragmentArgs by navArg<ProfileFragmentArgs>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return ComposeView(requireContext()).apply {
setContent {
Profile(profileViewModel = profileViewModel, id = profileFragmentArgs.id, navigateToFriendProfile = { findNavController().navigate(...) })
}
}
}
I have the single activity with several fragments on top, as Google recommends. In one fragment I wish to place a switch, and I wish to still know it's state when I come back from other fragments. Example: I am in fragment one, then I turn on the switch, navigate to fragment two or three, go back to fragment one and I wish to load that fragment with that switch in the on position as I left it.
I have tried to copy the examples provided by google advocates, just to see the code to fail hard and do nothing.
/////////////////////////////////////////////////////////////////
//Inside the first fragment:
class myFragment : Fragment() {
companion object {
fun newInstance() = myFragment()
}
private lateinit var viewModel: myViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.my_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
**viewModel = ViewModelProvider(this, SavedStateVMFactory(this)).get(myViewModel::class.java)
//Here I was hoping to read the state when I come back.
switch_on_off.isChecked = viewModel.getSwRoundTimerInit()**
subscribeToLiveData() //To read liveData
switch_on_off.setOnCheckedChangeListener { _, isChecked ->
viewModel.setOnOff(isChecked)
}
}//End of onActivityCreated
//other code...
/////////////////////////////////////////////////////////////////////////
//On the fragment ViewModel
class myViewModel(private val **mState: SavedStateHandle**) : ViewModel() {
//SavedStateHandle Keys to save and restore states in the App
private val swStateKey = "SW_STATE_KEY"
private var otherSwitch:Boolean //other internal states.
//Init for the other internal states
init {
otherSwitch = false
}
fun getSwRoundTimerInit():Boolean{
val state = mState[swStateKey] ?: "false"
return state.toBoolean()
}
fun setOnOff(swValue:Boolean){
mState.set(swStateKey, swValue.toString())
}
}
This does not work. It always loads the default (off) value, as if the savedState is null all the time.
change
//fragment scope
viewModel = ViewModelProvider(thisSavedStateVMFactory(this)).get(myViewModel::class.java)
to
//activity scope
viewModel = activity?.let { ViewModelProviders.of(it,SavedStateVMFactory(this)).get(myViewModel::class.java) }
https://developer.android.com/topic/libraries/architecture/viewmodel#sharing
I have an activity, TabBarActivity that hosts a fragment, EquipmentRecyclerViewFragment. The fragment receives the LiveData callback but the Activity does not (as proofed with breakpoints in debugging mode). What's weird is the Activity callback does trigger if I call the ViewModel's initData method. Below are the pertinent sections of the mentioned components:
TabBarActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initVM()
setContentView(R.layout.activity_nav)
val equipmentRecyclerViewFragment = EquipmentRecyclerViewFragment()
supportFragmentManager
.beginTransaction()
.replace(R.id.frameLayout, equipmentRecyclerViewFragment, equipmentRecyclerViewFragment.TAG)
.commit()
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
}
var eVM : EquipmentViewModel? = null
private fun initVM() {
eVM = ViewModelProviders.of(this).get(EquipmentViewModel::class.java)
eVM?.let { lifecycle.addObserver(it) } //Add ViewModel as an observer of this fragment's lifecycle
eVM?.equipment?.observe(this, loadingObserver)// eVM?.initData() //TODO: Not calling this causes Activity to never receive the observed ∆
}
val loadingObserver = Observer<List<Gun>> { equipment ->
...}
EquipmentRecyclerViewFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
columnCount = 2
initVM()
}
//MARK: ViewModel Methods
var eVM : EquipmentViewModel? = null
private fun initVM() {
eVM = ViewModelProviders.of(this).get(EquipmentViewModel::class.java)
eVM?.let { lifecycle.addObserver(it) } //Add ViewModel as an observer of this fragment's lifecycle
eVM?.equipment?.observe(this, equipmentObserver)
eVM?.initData()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_equipment_list, container, false)
if (view is RecyclerView) { // Set the adapter
val context = view.getContext()
view.layoutManager = GridLayoutManager(context, columnCount)
view.adapter = adapter
}
return view
}
EquipmentViewModel
class EquipmentViewModel(application: Application) : AndroidViewModel(application), LifecycleObserver {
var equipment = MutableLiveData<List<Gun>>()
var isLoading = MutableLiveData<Boolean>()
fun initData() {
isLoading.setValue(true)
thread { Thread.sleep(5000) //Simulates async network call
var gunList = ArrayList<Gun>()
for (i in 0..100){
gunList.add(Gun("Gun "+i.toString()))
}
equipment.postValue(gunList)
isLoading.postValue(false)
}
}
The ultimate aim is to have the activity just observe the isLoading MutableLiveData boolean, but since that wasn't working I changed the activity to observe just the equipment LiveData to minimize the number of variables at play.
To get same reference of ViewModel of your Activity you need to pass the same Activity instance, you should use ViewModelProviders.of(getActivity). When you pass this as argument, you receive instance of ViewModel that associates with your Fragment.
There are two overloaded methods:
ViewModelProvider.of(Fragment fragment)
ViewModelProvider.of(FragmentActivity activity)
For more info Share data between fragments
I put this code inside the onActivityCreated fragment, don't underestimate getActivity ;)
if (activity != null) {
globalViewModel = ViewModelProvider(activity!!).get(GlobalViewModel::class.java)
}
globalViewModel.onStop.observe(viewLifecycleOwner, Observer { status ->
Log.d("Parent Viewmodel", status.toString())
})
This code helps me to listening Parent ViewModel changes in fragment.
Just for those who are confused between definitions of SharedViewModel vs Making two fragments use one View Model:
SharedViewModel is used to share 'DATA' (Imagine two new instances being created and data from view model is being send to two fragments) where it is not used for observables since observables look for 'SAME' instance to take action. This means you need to have one viewmodel instance being created for two fragments.
IMO: Google should somehow mention this in their documentation since I myself thought that under the hood they are same instance where it is basically not and it actually now makes sense.
EDIT : Solution in Kotlin: 11/25/2021
In Your activity -> val viewModel : YourViewModel by viewModels()
In Fragment 1 - >
val fragmentViewModel =
ViewModelProvider(requireActivity() as YourActivity)[YourViewModel::class.java]
In Fragment 2 - >
val fragmentViewModel =
ViewModelProvider(requireActivity() as YourActivity)[YourViewModel::class.java]
This Way 2 fragments share one instance of Activity viewmodel and both fragments can use listeners to observe changes between themselves.
When you create fragment instead of getting viewModel object by viewModels() get it from activityViewModels()
import androidx.fragment.app.activityViewModels
class WeatherFragment : Fragment(R.layout.fragment_weather) {
private lateinit var binding: FragmentWeatherBinding
private val viewModel: WeatherViewModel by activityViewModels() // Do not use viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentWeatherBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
// Observing for testing & Logging
viewModel.cityName.observe(viewLifecycleOwner, Observer {
Log.d(TAG, "onCreateView() | City name changed $it")
})
return binding.root
}
}
Kotlin Answer
Remove these two points in your function if you are using:
= viewModelScope.launch { }
suspend