I have MainActivity which contains TabLayout with tabs: each tab is a fragment and each one has a RecyclerView. When I tap FAB in Main Activity, NewReminderActivity is opened.
I use Architecture Components: Entity(Reminder), DAO, Room, ViewModel, LiveData and Repository.
The question is:
Which methods should I use to deliver a new created reminder item into the fragment (which contains as mentioned above a RecyclerView?
I have some ideas, but could you help me please and give me a right direction for implementing:
1) I guess, I should deliver data to MainActivity, then from MainActivity to the fragment and use ViewModel as mentioned in https://developer.android.com/topic/libraries/architecture/viewmodel.html#sharing , am I right?
2) I guess I should use setResult() in NewReminderActivity, am I right?
If you are using Room, there is no reason for you to use setResult to transfer new item to any of these previous Fragments/Activities, because Room manages invalidation automatically.
#Dao
public interface MyDao {
#Query("SELECT * FROM ITEM")
LiveData<List<Item>> getItemsWithChanges();
#Insert(onConflict = OnConflictStrategy.REPLACE)
void insertItem(Item item);
}
Then
public class MyViewModel extends ViewModel {
private final LiveData<List<Item>> items;
public LiveData<List<Item>> getItems() {
return items;
}
public MyViewModel(MyDao myDao) {
items = myDao.getItemsWithChanges();
}
}
Then
public class MyFragment extends Fragment {
MyViewModel myViewModel;
#Override
protected void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
myViewModel = ViewModelProviders.of(getActivity(), viewModelFactory).get(MyViewModel.class);
myViewModel.getItems().observe(getViewLifecycleOwner(), (items) -> {
if(items != null) {
adapter.submitList(items);
}
});
}
}
In which case all you need to do in your second Activity is insert the new item, then finish out:
// imagine this is on background thread
myDao.insertItem(item);
runOnUiThread(() -> {
finish();
});
And all your RecyclerViews will update with the new item (if they are part of the results as the condition matches it).
Related
I just started using the architecture component of Android in my app, and I have a use case when:
A fragment is an observer of ViewModel's LiveData (which comes from a Room request)
This fragment starts an activity at some point
The activity needs to use the same data (as LiveData in the fragment) and update it
The user then returns to the fragment
I tried using the same ViewModel class and adding the fragment and activity as observers, thinking that somehow updating from the activity will affect the fragment when returning to it, but it doesn't work.
The two ViewModels seem independent.
How can I force refresh the fragment data when returning from the activity (in onActivityResult for example)?
Or is it possible to share the same ViewModel between the fragment and activity? (though I doubt it since the ViewModel is linked to the lifecycle of the observer)
Fragment
public void onAttach(#NonNull Context context) {
...
mViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
mViewModel.getData().observe(this, new Observer<List<Data>>() {
#Override
public void onChanged(List<Data> data) {
mAdapter.setData(data);
}
});
}
// in an onClick method
startActivity(intent); // go to the Activity
Activity
protected void onCreate(Bundle savedInstanceState) {
...
mViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
mViewModel.getData().observe(this, new Observer<List<Data>>() {
#Override
public void onChanged(List<Data> data) {
mPagerAdapter.setData(data);
}
});
}
Any help will be appreciated!
EDIT:
View Model
public class MyViewModel extends AndroidViewModel {
private Dao dao;
private ExecutorService executorService;
public MyViewModel (#NonNull Application application) {
super(application);
dao = AppDatabase.getInstance(application).getDao();
executorService = Executors.newSingleThreadExecutor();
}
public LiveData<Data> getData() {
return dao.getAllData();
}
// other methods to update and delete with executorService
}
I managed to get the result you want by seting a common lifecycle owner for the viewmodel, and using the same viewModel at the different fragments.
Inside my fragment, i get the view model like this:
INSIDE FRAGMENT:
var userViewModel = activity?.run{ViewModelProviders.of(this, SharedUserViewModel.MainActivityViewModelFactory(applicationContext))[SharedUserViewModel::class.java]}
Did you see i use the activity as the "ViewModelProviders.of" parameter?
This way, the view model have the same owner, its working alright for me.
Yes you can share ViewMode between activity and Fragment Using SharedViewModel.
let say for example I have 1 Activity that contains 5 Fragments and those Fragments presents a 1 flow of payment process so each Fragment depends on the previous Fragment by passing data of what the user chooses
I'm planning to make 1 ViewModel in the Activity that handles the data between fragments but I've read that it is a bad idea to expose MutableLiveData outside of the view model. so I can't say viewModel.setdata(example) in the Activity the best solution was is to use navigation component with safe args and create a ViewModel for each Fragment and create ViewModelFactory for each fragment too.
but this will make me write too many classes.
is there an optimal way to pass data between views using 1 ViewModel without violating the MVVM architecture rules?
Yes, this is good decision to use ViewModel to share data between fragments. Look this
SharedViewModel
public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
MasterFragment
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onViewCreated(#NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
DetailFragment
public class DetailFragment extends Fragment {
public void onViewCreated(#NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
SharedViewModel model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
model.getSelected().observe(getViewLifecycleOwner(), item -> {
// Update the UI.
});
}
}
Here is the black magic:
val viewModel: MyViewModel by activityViewModels()
In this problem I want to get Data from My API
In which I get Data from Retrofit
I want to show data in RecyclerView in Fragment Tabs but how can I send data from activity to Fragment
This is all I have tried
Retrofit call which provide me ArrayList of my posts
getMainApp().swiftAPI.getPosts().enqueue(object : Callback<ArrayList<Post>>{
override fun onFailure(call: Call<ArrayList<Post>>?, t: Throwable?) {
Toast.makeText(this#DashboardActivity, t?.message, Toast.LENGTH_SHORT)
}
override fun onResponse(call: Call<ArrayList<Post>>?, response: Response<ArrayList<Post>>?) {
if (response?.isSuccessful!!){
}
}
PagesFragment
val rootView = inflater.inflate(R.layout.fragment_page, container, false)
val video_recyclerview = rootView.findViewById(R.id.pages_rcv) as RecyclerView // Add this
video_recyclerview.layoutManager = LinearLayoutManager(activity)
video_recyclerview.adapter = PagesAdapter()
return rootView
I want to Know if there is any way possible to send ArrayList to fragment cause my data is in ArrayList
You can define an interface in your activity and let the fragment implement the interface. You can follow this example on my github: ActivityToFragmentCommunication
Basically, in your activity define:
public interface DataLoadedListener {
public void onDataLoaded(ArrayList<Post> posts);
}
Then, make your fragment implement the interface like below:
public class ExampleFragment extends Fragment implements MainActivity.DataLoadedListener {
// your fragment code
}
Finally in the onCreate() method of your activity:
// Create new fragment and transaction
mExampleFragment = new ExampleFragment();
// setting mExampleFragment as data load listener
mDataLoadedListener = (DataLoadedListener) mExampleFragment;
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack if needed
transaction.replace(R.id.flContainer, mExampleFragment);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
// load data after click
btLoadData.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
loadData();
// notify attached fragment
mDataLoadedListener.onDataLoaded(myStrings);
}
});
First check currentFragment in your activity class. You will get the fragment reference object in your activity class . so the example will like below:
Suppose you have a fragment called DataFragment and you have a reference mDataFragment in your activity class. now when you get data in your activity class you will call ((DataFragment)mDataFragment).passData(yourDataList). Remember passData() is a public method in your fragment class. Then you can add data in adapter and call notifyDataSetChanged()
Since you are working on Android, I would recommend the ViewModel component which makes it really easy to communicate between a activity and it's fragments.
First add the package to your app
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
Then create a ViewModel class
public class MyViewModel extends ViewModel {
public MutableLiveData<ArrayList<Post>> posts = new MutableLiveData<ArrayList<Post>>();
}
Now in the fragment subscribe to it.
public class DetailFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.posts.observe(this, { posts ->
// Update the UI.
});
}
}
Then set the value in your MainActivity as shown below and voila you have the data in your fragment. You can read more about it here
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same MyViewModel instance created by the first activity.
MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
model.users.setValue(posts);
}
}
LiveData/ ViewModel is a good replacement for complicated Loader.
Based on https://medium.com/google-developers/lifecycle-aware-data-loading-with-android-architecture-components-f95484159de4 ,
AsyncTask is member of LiveData.
public class JsonLiveData extends LiveData<List<String>> {
public JsonLiveData(Context context) {
loadData();
}
private void loadData() {
new AsyncTask<Void,Void,List<String>>() {
}.execute();
}
}
However, based on the presentation from Lyla Fujiwara:
Should I make AsyncTask member of Repository class?
You should avoid running your AsyncTask in LiveData. LiveData should really only be concerned with the observation of data. Not the act of changing the data.
The best way of dealing with this situation is to use the ViewModel / Repository pattern.
Activity / Fragment observes LiveData from ViewModel, ViewModel observes LiveData from Repository. Changes are made in the repository, which are pushed to its LiveData. Those changes are delivered to the Activity / Fragment (through the ViewModel).
I would avoid using AsyncTask in this situation. The bonus of AsyncTask is that you can get results on the UI thread after doing work. In this case though, that isn't necessary. LiveData will do that for you.
Here is an (untested) example:
Activity
public class MyActivity extends AppCompatActivity {
private MyViewModel viewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set up your view model
viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
// Observe the view model
viewModel.getMyLiveData().observe(this, s -> {
// You work with the data provided through the view model here.
// You should only really be delivering UI updates at this point. Updating
// a RecyclerView for example.
Log.v("LIVEDATA", "The livedata changed: "+s);
});
// This will start the off-the-UI-thread work that we want to perform.
MyRepository.getInstance().doSomeStuff();
}
}
ViewModel
public class MyViewModel extends AndroidViewModel {
#NonNull
private MyRepository repo = MyRepository.getInstance();
#NonNull
private LiveData<String> myLiveData;
public MyViewModel(#NonNull Application application) {
super(application);
// The local live data needs to reference the repository live data
myLiveData = repo.getMyLiveData();
}
#NonNull
public LiveData<String> getMyLiveData() {
return myLiveData;
}
}
Repository
public class MyRepository {
private static MyRepository instance;
// Note the use of MutableLiveData, this allows changes to be made
#NonNull
private MutableLiveData<String> myLiveData = new MutableLiveData<>();
public static MyRepository getInstance() {
if(instance == null) {
synchronized (MyRepository.class) {
if(instance == null) {
instance = new MyRepository();
}
}
}
return instance;
}
// The getter upcasts to LiveData, this ensures that only the repository can cause a change
#NonNull
public LiveData<String> getMyLiveData() {
return myLiveData;
}
// This method runs some work for 3 seconds. It then posts a status update to the live data.
// This would effectively be the "doInBackground" method from AsyncTask.
public void doSomeStuff() {
new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException ignored) {
}
myLiveData.postValue("Updated time: "+System.currentTimeMillis());
}).start();
}
}
Let's say that we have two fragments: MainFragment and SelectionFragment. The second one is build for selecting some object, e.g. an integer. There are different approaches in receiving result from this second fragment like callbacks, buses etc.
Now, if we decide to use Navigation Architecture Component in order to navigate to second fragment we can use this code:
NavHostFragment.findNavController(this).navigate(R.id.action_selection, bundle)
where bundle is an instance of Bundle (of course). As you can see there is no access to SelectionFragment where we could put a callback. The question is, how to receive a result with Navigation Architecture Component?
They have added a fix for this in the 2.3.0-alpha02 release.
If navigating from Fragment A to Fragment B and A needs a result from B:
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<Type>("key")?.observe(viewLifecycleOwner) {result ->
// Do something with the result.
}
If on Fragment B and need to set the result:
findNavController().previousBackStackEntry?.savedStateHandle?.set("key", result)
I ended up creating two extensions for this:
fun Fragment.getNavigationResult(key: String = "result") =
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>(key)
fun Fragment.setNavigationResult(result: String, key: String = "result") {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}
Since Fragment KTX 1.3.6 Android supports passing data between fragments or between fragments and activities. It's similar to startActivityForResult logic.
Here is an example with Navigation Component. You can read more about it here
build.gradle
implementation "androidx.fragment:fragment-ktx:1.3.6"
FragmentA.kt
class FragmentA : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// Step 1. Listen for fragment results
setFragmentResultListener(FragmentB.REQUEST_KEY) { key, bundle ->
// read from the bundle
}
// Step 2. Navigate to Fragment B
findNavController().navigate(R.id.fragmentB)
}
}
FragmentB.kt
class FragmentB : Fragment() {
companion object {
val REQUEST_KEY = "FragmentB_REQUEST_KEY"
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
buttonA.setOnClickListener { view ->
// Step 3. Set a result
setFragmentResult(REQUEST_KEY, bundleOf("data" to "button clicked"))
// Step 4. Go back to Fragment A
findNavController().navigateUp()
}
}
}
Use these extension functions
fun <T> Fragment.getNavigationResult(key: String = "result") =
findNavController().currentBackStackEntry?.savedStateHandle?.get<T>(key)
fun <T> Fragment.getNavigationResultLiveData(key: String = "result") =
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)
fun <T> Fragment.setNavigationResult(result: T, key: String = "result") {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}
So if you want to send result from Fragment B to fragment A
Inside Fragment B
setNavigationResult(false, "someKey")
Inside Fragment A
val result = fragment.getNavigationResultLiveData<Boolean>("someKey")
result.observe(viewLifecycleOwner){ booleanValue-> doSomething(booleanValue)
Important note
In the Fragment B you need to set the result (setNavigationResult())
in the started or resumed state (before onStop() or onDestroy()),
otherwise previousBackStackEntry will be already null.
Important note #2
If you’d only like to handle a result only once, you must call
remove() on the SavedStateHandle to clear the result. If you do not
remove the result, the LiveData will continue to return the last
result to any new Observer instances.
More information in the official guide.
According to Google: you should try to use shared ViewModel. Check below example from Google:
Shared ViewModel that will contain shared data and can be accessed from different fragments.
public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
MasterFragment that updates ViewModel:
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
DetailsFragment that uses shared ViewModel:
public class DetailFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, item -> {
// Update the UI.
});
}
}
with a little improvement of #LeHaine 's answer, you can use these methods for navigation 2.3.0-alpha02 and above
fun <T> Fragment.getNavigationResult(key: String) =
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)
fun <T> Fragment.setNavigationResult(result: T, key: String) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}
I created a wrapper function that's similar to LeHaine's answer, but it handles more cases.
To pass a result to a parent from a child:
findNavController().finishWithResult(PickIntervalResult.WEEKLY)
To get result from a child in a parent:
findNavController().handleResult<PickIntervalResult>(
viewLifecycleOwner,
R.id.navigation_notifications, // current destination
R.id.pickNotificationIntervalFragment // child destination
) { result ->
binding.textNotifications.text = result.toString()
}
My wrapper isn't so simple as LeHaine's one, but it is generic and handles cases as:
A few children for one parent
Result is any class that implements Parcelable
Dialog destination
See the implementation on the github or check out an article that explains how it works.
fun <Type> BaseNavigationActivity<*,*,*>.getNavigationResult(key : String, #IdRes viewId: Int) =
this.findNavController(viewId).currentBackStackEntry?.savedStateHandle?.getLiveData<Type>(key)
fun <Type> BaseNavigationActivity<*,*,*>.setNavigationResult(result: Type, key: String, #IdRes viewId: Int){
this.findNavController(viewId).previousBackStackEntry?.savedStateHandle?.set<Type>(key, result)
}
I would suggest you to use NavigationResult library which is an add-on for JetPack's Navigation Component and lets you to navigateUp with Bundle.
I've also wrote a blog post about this on Medium.
Just an alternative to the other answers...
EventBus with MutableShareFlow as it core in a shared object (ex: repo) and an observer describe in here
Looks like things are moving away from LiveData and going in Flow direction.
Worth to have a look.