I have question about programmatically generated fragments in the android.
Now I'm creating app that helps in training on the gym. I want to create something like "live training" - that means user will have some exercises, each exercise will have weight to lift etc.
I was thinking that each exercise would be a fragment (and have own data), but now I think about how to communicate to autogenerated fragments from parent activity to e.g read some data.
The user should be able to switch between exercises (so any series and weight should be saved for each exercise
until the workout is finished), but now i'm looking for best solution for that and I'm stuck :/. I've never done such a thing before.
Use Model View View Model Architecture with live data for observing data changes.
Architecture Components provides ViewModel helper class for the UI controller that is responsible for preparing data for the UI. ViewModel objects are automatically retained during configuration changes so that data they hold is immediately available to the next activity or fragment instance.
View Model
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;
}
}
Master Fragment
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);
});
}
}
Details Fragment
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.
});
}
}
For further information explore the android developer documentation for MVVM Architecture
https://developer.android.com/topic/libraries/architecture/viewmodel
Related
I'm trying to store the value of my fragments components inside an activity with lower navigation menu, controlled by navController.
My problem is that when I change one fragment to another (onPause) with the bottom navigation, or rotate the screen, all my data in the component layout is lost.
is there a solution to solve this problem?
Store the data in a Viewmodel and observe it from the UI. The stored data is not lost if the framework destroys and re-creates the activities and fragments during a configuration change or other events.
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=en
Create class that extends ViewModel and store your variable in it as LiveData like in this example:
public class MyViewModel extends ViewModel {
private MutableLiveData<List<User>> users;
public LiveData<List<User>> getUsers() {
if (users == null) {
users = new MutableLiveData<List<User>>();
loadUsers();
}
return users;
}
private void loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
Then you can access the viewmodel from the activity and fragments like this.
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 = new ViewModelProvider(this).get(MyViewModel.class);
model.getUsers().observe(this, users -> {
// update UI
});
}
For further details check the documentation on the link above.
I basically have a MainActivity that has multiple tabs. Each tab is a ShowListFragment and it extends Fragment. Now, each tab contains data that I fetch from a database. I have a MOVE-button that moves data from one tab to another in each Fragment:
#Override
public void onClick(DialogInterface dialog, int listIndex) {
database.add(listIndex,object);
database.remove(listIndex,object);
}
The fragment does not update directly, but after a few swipes in between the tabs (3 exactly). How do I force the Fragment to update instantaneous after I clicked the button? I don't want to manage it through onPageSelected in the ViewPager, since it does not update the fragment I'm currently on, but after I've swiped to the next fragment. And also I don't want to update the data after each swipe.
I know that I maybe need to use some kind of observer pattern like this: How do I make a Class extend Observable when it has extended another class too?
But still, I'm still not sure how to update the fragment directly, and how to apply the observer/event pattern in my application.
Updated Answer:
With Android architecture components, doing this is much simpler.
The recommended pattern is using a ViewModel with LiveData members. Your fragments will register observers on the LiveData members which will automatically be coordinated with lifecycle events, e.g. unregistering in onDestroy() etc. https://developer.android.com/topic/libraries/architecture/livedata
When using the Navigation component, you can pass data when navigating to a fragment: https://developer.android.com/guide/navigation/navigation-pass-data
You can also return data from the navigated fragment: https://developer.android.com/guide/navigation/navigation-programmatic
Old answer (superseded by Architecture Components):
Since the fragments can access the activity easily enough with getActivity(), I would make the activity be the central hub for dispatching updates.
It sounds like you already have the persistence part handled with the database and all you need is some update events. So here goes:
Define a listener interface. I usually do this as an inner interface within the activity:
public interface DataUpdateListener {
void onDataUpdate();
}
Add a data structure to your activity to keep track of listeners:
private List<DataUpdateListener> mListeners;
Don't forget to initialize in the constructor:
mListeners = new ArrayList<>();
Add the register/unregister methods to the activity:
public synchronized void registerDataUpdateListener(DataUpdateListener listener) {
mListeners.add(listener);
}
public synchronized void unregisterDataUpdateListener(DataUpdateListener listener) {
mListeners.remove(listener);
}
Add the event method to your activity:
public synchronized void dataUpdated() {
for (DataUpdateListener listener : mListeners) {
listener.onDataUpdate();
}
}
Have your fragments implement DataUpdateListener:
public class MyFragment extends Fragment implements DataUpdateListener {
and implement the method
#Override
public void onDataUpdate() {
// put your UI update logic here
}
Override onAttach() and onDestroy() in the fragments to register/unregister:
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
((MainActivity) activity).registerDataUpdateListener(this);
}
#Override
public void onDestroy() {
super.onDestroy();
((MainActivity) getActivity()).unregisterDataUpdateListener(this);
}
Fire the event in your fragment's UI update event:
#Override
public void onClick(DialogInterface dialog, int listIndex) {
database.add(listIndex,object);
database.remove(listIndex,object);
((MainActivity) getActivity()).dataUpdated();
}
I've written a pretty large custom view which overrides onSaveInstanceState() and onRestoreInstanceState(Parcelable state).
I wanted to populate a LinearLayout with my custom view, so I wrote the following code:
public class MainActivity extends Activity {
private LinearLayout mRootLayout;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRootLayout = (LinearLayout) findViewById(R.id.root_layout);
int i;
// Test: adding 10 instances of MyCustomView.
for (i = 0; i < 10; ++i) {
MyCustomView cv = new MyCustomView(this);
// I set an ID for this view so that onSaveInstanceState() and
// onRestoreInstanceState(Parcelable state) will be called
// automatically.
cv.setId(++i);
mRootLayout.addView(cv);
}
}
// ...
}
It works fine - mRootLayout is indeed being populated with 10 instances of MyCustomView, and each instance of MyCustomView is being properly restored after, for example, screen rotation.
I've noticed that due to the fact that MyCustomView is pretty large, my code is being heavy on the UI thread.
To solve the issue and take some effort off of the UI thread, I decided to use a custom AsyncTask, which will create an instance of MyCustomView in doInBackground() and add it to the the main layout ( mRootLayout ) in onPostExecute().
The following code is my custom AsyncTask:
private class LoadMyCustomViewTask extends AsyncTask<Void, Void, MyCustomView> {
private Context mContext;
private LinearLayout mLayoutToPopulate;
private int mId;
public LoadMyCustomViewTask(Context context, LinearLayout layout, int id) {
mContext = context;
mLayoutToPopulate = layout;
mId = id;
}
#Override
protected MyCustomView doInBackground(Void... params) {
MyCustomView cv = new MyCustomView(mContext);
return cv;
}
#Override
protected void onPostExecute(MyCustomView result) {
result.setId(mId);
mLayoutToPopulate.addView(result);
}
}
In MainActivity I use it as follows:
public class MainActivity extends Activity {
private LinearLayout mRootLayout;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRootLayout = (LinearLayout) findViewById(R.id.root_layout);
int i;
for (i = 0; i < 10; ++i) {
new LoadMyCustomViewTask(this, mRootLayout, ++i).execute();
}
}
// ...
}
This code works too, but there is only one problem - MyCustomView is not being restored at all.
For debug purposes I put a Log.d(...) in MyCustomView's onSaveInstanceState() and in onRestoreInstanceState(Parcelable state), and I've noticed that onRestoreInstanceState(Parcelable state) isn't being called.
Do you have any idea why onRestoreInstanceState(Parcelable state) isn't being called when I use an AsyncTask to populate mRootLayout, but it is indeed being called when I create MyCustomView completely on the UI thread?
Thank you.
Edit: I'm posting the methods onSaveInstanceState() and onRestoreInstanceState() of MyCustomView
#Override
protected Parcelable onSaveInstanceState() {
debug("onSaveInstanceState()");
Bundle state = new Bundle();
state.putParcelable(_BUNDLE_KEY_PARENT_STATE, super.onSaveInstanceState());
state.putBooleanArray(_BUNDLE_KEY_CLICKED_VIEWS, mClickedViews);
return state;
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
debug("onRestoreInstanceState(Parcelable state)");
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
mClickedViews = bundle.getBooleanArray(_BUNDLE_KEY_CLICKED_VIEWS);
state = bundle.getParcelable(_BUNDLE_KEY_PARENT_STATE);
}
super.onRestoreInstanceState(state);
}
View state restoration begins at the root view and moves down to all of the child views attached at that time. This can be seen in the ViewGroup.dispatchRestoreInstanceState method. This means that Android can only restore your views if they are part of the view hierarchy at the time Activity.onRestoreInstanceState is called.
Using the AsyncTask, you are creating your views asynchronously and then scheduling them to be added some time later when the main looper is idle. Considering the lifecycle, Android only lets your AsyncTask.onPostExecute run after Activity.onStart, Activity.onRestoreInstanceState, Activity.onResume, etc. are called. Your views are being added to the layout too late for automatic restoration to take place.
If you add log statements to those methods mentioned above, as well as to your AsyncTask.onPostExecute, you will be able to see how the ordering/timing plays out in reality. The following code runs after Activity.onRestoreInstanceState even though it all happens on the main thread, simply because of the scheduling:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
new Handler().post(new Runnable() {
#Override
public void run() {
Log.i("TAG", "when does this run?");
}
});
...
}
Smells like a false observation... creating a view on a background thread should not affect your activity lifecycle.
That said, doing anything at all with View objects on background threads is a no-no and I'm surprised you got this far with such an approach. All View code should be quick and avoid blocking. If you have long-running work to do then separate that work into the background thread, post the results of that complex computation to the main thread, and keep all the actual View/presentation stuff on the main thread where it belongs.
I remember having read about how onSaveInstanceState()/onRestoreInstanceState() work a time ago and it was not a "fixed-situation-rules" thing. As I can't find the references to it at this moment, I'll try to explain it with my own words:
Basically, a factor on which depends the calling of those methods is the resources left that the device has. Both methods will get in action when a second Activity gets focused and the first one gets killed due to lack of resources. Concretely, onRestoreInstanceState should be triggered when that Activity was killed and restarted, so it gets the previous state.
Although you've not posted your MyCustomView implementation, my guess is that when you do that entirely on the main UI Thread, you're involving some action that makes the MainActivity lose its focus and once the MyCustomView is created, it needs to restore its state. Doing this in a separate thread (as AsyncTask does) makes creating those Views in paralell, so your main Activity doesn't lose its focus and thus it doesn't get killed, so those methods are not called.
Concluding this, don't worry if those methods are not always called as they don't have to be called everytime, just when needed, and that doesn't mean there's something going wrong.
I recommend you to connect SDK sources to your IDE and walk there with debugger through the whole process related with the View class onRestoreInstanceState(). Since I don't have your code, looking at sources I can only guess what might have gone wrong, but from what I see, that might be related to problem:
1) try to set an Id to every view you generate
2) try to use Fragment as a host to your views (instead of MainActivity).
I am writing an Android app (4.4) that uses Fragments. Each Fragment is in it's own .java file (and its own class), and each one has it's own .XML (layout) file. In the main FragmentActivity, my "getItem" routine reads the "position" argument, and creates instances of these classes as needed.
When the app starts, when Fragment 0 (zero) starts up, it runs some code in the "onCreateView." Based on what happens in that code, I need to change the UI of the Fragment 1 (buttons appear & disappear based on that logic).
However, the code RUNS with no errors, but the UI changes do not take effect. I'm thinking that perhaps I need to run my "startup" code somewhere else with a wider scope. I could be wrong.
Can anyone suggest a way for me to be able to control the UI of various layouts at startup?
Thanks!
If you can post some of your code, would be easier.
anyway if I got your problem, you need to change the UI of the fragment 1 from the fragment 0.
What you need is what is explained in the document Communicating with Other Fragments
you should do something like:
public class MyActivity extends FragmentActivity implements MyInterface{
#Override
public void changeUI(String sometext) {
Fragment1 fragment1 = (Fragment1) getFragmentManager().findFragmentByTag("tagCommittedFragment1");
fragment1.applyChange(sometext);
}
}
public class Fragment0 extends Fragment{
MyInterface mMyInterface;
public interface MyInterface {
public void changeUI(String sometext);
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mMyInterface = (MyInterface) activity;
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mMyInterface.changeUI("newtext");
}
}
public class Fragment1 extends Fragment{
public void applyChange(String sometext){
// do your work
}
}
You must make an interface to communicate between Fragments which would be implemented by your MainActivity:
public interface Communicator {
public void respond(String data);
}
Now you need to use this Interface to send data from FragmentA:
Communicator comm = getAcitivity(); //your activity must implement this interface
comm.respond(data);
As your MainActivity implements the above interface, it will also implement the respond() method which can be used to pass data to FragmentB:
public void respond(String data){
FragmentManager manager = getSupportFragmentManager();
FragmentB fragB = manager.findFragmentById(R.id.fragment_b);
fragB.changeData(data);
}
Now all you need to do is collect this data and make changes in FragmentB using a changeData() method:
public void changeData(String data){
textView.setText(data);
}
NOTE: As FragmentB has no use of the Interface, it should not be visible to it, therefore you can also create the interface inside FragmentA instead.
Right now I have my ApplicationActivity, this activity is responsible for managing multiple views (GLSurfaceViews). Can / Should I have all the views set the renderer to a "global" renderer?
Code:
public class ApplicationActivity extends Activity
{
private static final String TAG = ApplicationActivity.class.getSimpleName();
private final Stack<Screen> screens = new Stack<Screen>();
private GlRenderer glRenderer;
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Log.d(TAG, "Main Activity Created");
setupGraphics();
ChangeScreen(new MainMenu(this, glRenderer)); //Creating a new Screen sets the renderer
}
private void setupGraphics()
{
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
glRenderer = new GlRenderer(this);
}
public void Draw() //Is called by the glRenderer onDrawFrame() { mainActivity.Draw() }
{
}
}
Its the same activity switching between GLSurfaceViews and by my knowledge I believe that the method setRenderer sets the view renderer and then starts the rendering thread (creating a new thread) but I don't want to recreate the thread every time I switch between views - may create potential problems.
So in the end I want a Renderer class just to keep graphics sepreate from business logic and such but, I don't know if using one Renderer is even possible, without setting the thread again?
You can only use Multiple Views with the same Renderer only if you properly switch out between them with GLSurfaceView.onPause() / .onResume();
My specific case:
#Override
protected void onPause() //Overrides onPause from Activity
{
surfaceViews.peek().onPause();
super.onPause();
}
So everytime the activity pauses I would have to pause the current View. And if the Activity resumes then resume the View also.
I also have a method called SetView which will either (pause and remove then change to another View) or (pause and then change to another View) this is accomplished using a Stack
public void SetView(View screen)
{
if (!screens.empty())
{
screens.peek().onPause();
screens.pop();
}
screens.push(screen);
setContentView(screens.peek());
}
Of course though because we are using Views instead of Activities now we must Override the onBackPressed() to go back to previous Views.
#Override
public void onBackPressed()
{
if (screens.size() == 1)
super.onBackPressed();
else
{
screens.pop();
setContentView(screens.peek());
screens.peek().onResume();
}
}
By doing new GLRenderer() you create new instance of your class. So there is no problem to have the same renderer used in different activities.
EDIT: I seem to misunderstand your question - if you want many GL surfaces visible at once, then no, it is not possible. But it got nothing to do with reusing renderer code.