I have a fragment which has a TextView, an EditText and a Button. I also have 2 activities which include this fragment and at onClick of the button in one of the activities, the other is started. Via the intent, the text in the edittext is passed which becomes the text of the textview of the other activity.
I had two design decisions to choose from
Create two such fragments classes with appropriate methods that construct the appropriate intents. Access the UI elements from inside the respective fragment object and start the activities.
Create only one fragment class. onClick the, event is passed down to a particular method in the activities (both the activities have this method) and the activities have the logic to build the intent and start the other activity
Consider what would happen if there are 100 such activities. The first method would have us write 100 different fragment classes with custom methods, but in the second method, it is a single class and the activities have the custom logic in a particularly named method.
Therefore I chose to go with the second choice and I realized that the UI elements could not be instantiated in the onCreate method of activity as the fragment's layout is not inflated yet. I am doing the instantiation in onStart as a workaround.
Is that bad practice or is there a better design pattern to follow?
The recommended pattern is to create a holder interface which any activity that wants to instantiate your fragment must implement. Also to set data for views in your new fragment then create a newInstance() factory method on your fragment.
I tend to approach it like this;
class FooFragment implements Fragment {
private static final String TEXT_FOR_TEXTVIEW = "textForTextView";
private FooFragmentHolder mHolder;
/*
* Rather than creating your fragment in your layout directly
* you should instead instantiate it using this class in your
* activity.
*/
public static FooFragment newInstance(String text) {
Bundle data = new Bundle();
data.putString(TEXT_FOR_TEXTVIEW, text);
FooFragment fooFragment = new FooFragment();
fooFragment.setArguments(data);
return fooFragment;
}
public interface FooFragmentHolder {
public void buttonPressed(String editTextContent);
}
/*
* When we create the fragment with the activity we use onAttach to get
* our holder implementation (the activity)
*/
#Override
public void onAttach(Activity activity) {
if (activity instanceof FooFragmentHolder) {
mHolder = (FooFragmentHolder) activity;
} else {
throw new IllegalStateException("Containing activity must implement FooFragmentHolder");
}
}
#Override
public void onCreateView(Inflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_foo, container, false);
final EditText editText = (EditText) view.findViewById(R.id.edit_text);
Button button = (Button) view.findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
#Override
public void onClick(Button button) {
mHolder.buttonPressed(editText.getText());
}
})};
TextView textView = (TextView) view.findViewById(R.id.text_view);
Bundle args = getArguments();
if (args != null) {
textView.setText(args.getString(TEXT_FOR_TEXTVIEW));
}
return view;
}
}
Now in your activity you just need to implement the FooFragmentHolder interface and use the newInstance method we created;
class FooActivity extends Activity implements FooFragment.FooFragmentHolder {
private static final String TEXT_FOR_TEXTVIEW = "textForTextView";
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentLayout(R.layout.activity_foo);
// Instead of creating your fragment in your layout, create a holder
// layout and attach a new instance of your fragment using a fragment
// transaction.
FooFragment fooFragment = FooFragment.newInstance(getIntent().getStringExtra(TEXT_FOR_TEXTVIEW));
getFragmentManager().beginTransaction()
.replace(R.id.content, fooFragment)
.commit();
}
#Override
public void buttonPressed(String editTextContent) {
// In this case just starting the next FooActivity, but logic could be
// applied for any other activity.
Intent intent = new Intent(this, FooActivity.class)
.putExtra(TEXT_FOR_TEXTVIEW, editTextContent);
startActivity(intent);
}
}
I decided to settle with the following patter --
Any activity which includes this fragment should implement an interface like
public interface ViewsCreatedListener {
public void onViewsCreated();
}
The activity would then look like
public class ExampleActivity extends Activity implements ViewsCreatedListener {
.
.
.
.
#Override
public void onViewsCreated() {
//Initiate the views here and do what gotta be done
}
}
The fragment should check that any activity that includes this fragment should implement that interface using the onAttach method and onActivityCreated, the activity is notified
public class ExampleFragment extends Fragment {
ViewsCreatedListener listener = null;
.
.
.
.
#Override
public onAttach(Activity activity) {
super.onAttach(activity);
try {
listener = (ViewsCreatedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement ViewsCreatedListener");
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
listener.onViewsCreated();
}
}
Doing this way, the fragment just provides the UI and the including activities decide as to what should be done with the UI elements included via the fragment. This maximizes reusability.. DRY... :-D
Related
I have an activity with two fragment and want to be executed first fragment when its back from second fragment using back button. And i am using the add() when navigating first fragment to second fragment. Here is my scenario and code snippet:
First fragment:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.one_fragment, container, false);
final Button button = (Button) view.findViewById(R.id.buttonChange);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
buttonClicked(v);
}
});
return view;
}
public void buttonClicked(View view) {
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.fragment_Container, new TwoFragment());
fragmentTransaction.addToBackStack("sdfsf");
fragmentTransaction.commit();
}
Moving to Second fragment and here is the code:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.two_fragment, container, false);
return view;
}
The problem is that, When I am navigating from first to second fragment and then back again in the first fragment using back button first fragment lifecycle method is not executing. Instead of using add() if I use replace() then lifecycle method are executing properly. I know its the difference between add() and replace() but I want to use add() and also want to have navigation callback to handle some logic when I back in the first fragment using back button.
Also tried below code:
fragmentManager.addOnBackStackChangedListener(
new FragmentManager.OnBackStackChangedListener() {
public void onBackStackChanged() {
Log.e(TAG, "onBackStackChanged: ");
// Update your UI here.
}
});
But its also calling multiple times and creating anomalies.
How can I handle this? Specially handle some logic in first fragment when I back from second fragment.
The easiest way I can think of is to set result when you're done with the second fragment that essentially tells the first fragment to "resume" via its onActivityResult method.
When you create an instance of Fragment B, call #setTargetFragment() and pass in Fragment A as your target fragment. Then when Fragment B is done and going to return to Fragment A, before it exits, you will set the result of it for Fragment A by calling:
getTargetFragment().onActivityResult(getTargetRequestCode(), RESULT_FRAGMENT_B_FINISHED,null)
///// horizontal scroll padding
Note that RESULT_FRAGMENT_B_FINISHED would be some static integer you define somewhere, like
public static final int RESULT_FRAGMENT_B_FINISHED = 123123;
Now in Fragment A all you need to do is override onActivityResult and check that the request code matches the request code integer from setTargetFragment and the result code also matches RESULT_FRAGMENT_B_FINISHED, if so you can run the code that would have been fired from onResume().
#getTargetFragment()
#onActivityResult()
#getTargetRequestCode()
Instead of passing data between the two fragments I recommend you to use a SharedViewModel.
The idea is that the first fragment observe some data for changes and the second one edit this data.
Example:
Shared ViewModel
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;
}
}
First fragment
public class FirstFragment 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.
});
}
}
Second fragment
public class SecondFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedViewModel model =
ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.select(new Item("value 1","value 2");
}
}
You can read about ViewModels, LiveData and Architecture components starting from here: https://developer.android.com/topic/libraries/architecture/viewmodel#java
This answer is assuming that you want to execute some logic based on some data change. If it's not the case, you can explain what kind of logic do you want to execute and I will edit my answer.
I am trying to build an app with one MainActivity and multiple fragments.
In my MainActivity, I get the data and store it in the Data Model.
For example, getting sunrise time, then display it in Fragment B.
How can I detect the sunrise value changes and update the TextView in Fragment without restarting the app? Is there way can listen to value changed and update the textView?
here are my codes and fragment B layout.
JAVA data model CLASS
public class SunriseTimeClass {
private static final SunriseTimeClass INSTANCE = new SunriseTimeClass();
public String sunrise = "";
private SunriseTimeClass(){ }
public static SunriseTimeClass getInstance(){
return INSTANCE;
}
}
MAINACTIVITY
override fun onCreate(savedInstanceState: Bundle?) {
//this will clear the back stack and displays no animation on the screen
var sunRise = SunriseTimeClass.getInstance()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
supportActionBar!!.setDisplayShowHomeEnabled(true)
getSunriseSunset()
}
fun getSunriseSunset(){
val location = com.luckycatlabs.sunrisesunset.dto.Location("40.9167654", "-74.171811")
val calculator = SunriseSunsetCalculator(location, "America/New_York")
sunRise.sunrise = calculator.getOfficialSunriseForDate(Calendar.getInstance())
}
FRAGMENT B layout
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text=""
android:gravity="center"
android:id="#+id/DisplaySunrise"/>
</FrameLayout>
(Java code)
Create a setter method in fragment which will be called by activity like:
In fragment:
public void setSunrise(String sunrise){
this.sunrise = sunrise;
//further operations..
}
And in Activity:
Fragment fragment = new Fragment(); //your fragment
String s= calculator.getOfficialSunriseForDate(Calendar.getInstance());
fragment.setSunrise(s);
If I understood you right, you need change/update the fragment when something happens in the Activity.
So I think you could implement an interface ( callback ) between the Activity and the Fragment.
1.You need create an interface in the Activity:
public interface OnActivityAction {
void updateFragment();}
2.You need declare the interface and the setter in the Activity and call it when you want
public OnActivityAction actionListener;
public void setOnActionListener(OnActivityAction fragmentListener) {
this.actionListener = fragmentListener;
}
private void myActionToUpdate(){
if (null != activityListener) {
activityListener.doSomethingInFragment();
}
}
3.Then in your Fragment need implement this interface and set the listener.
public class MyFragment extends Fragment implements OnActivityAction {
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View rootView = inflater.inflate(R.layout.fragment_test_two, container, false);
((MainActivity) getActivity()).setOnActionListener(this);
return rootView;
}
#Override
public void updateFragment() {
//Do whatever you want
}
}
I hope this helps you.
Add fragment from activity
FragmentManager fragmentManager = getSupportFragmentManager();
android.support.v4.app.FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
//frame_layout put in activity xml
fragmentTransaction.add(R.id.frame_layout, drawer, "").commit();
Get Fragment instance and update your data
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(R.id.frame_layout);
((FragmentB)fragment).updateFragment("Updated value");
Make Public method in fragment for update textview
public void updateFragment(String value) {
//update textview here
Textview.setText(value);
}
Thanks for all the great answers.
I have used SharePreference to do my job. working as expected.
There are two possible ways to approach this.
FragmentB gets the sunRise data and updates its own TextView
Have FragmentB retrieve the sunRise values directly and update its TextView, rather than performing the retrieval in MainActivity.
OR
Communicate between Activity and Fragment via an interface
This answer is similar to another one in this thread, but is more explicit on how you would set the fragment's TextView. This solution makes several assumptions and may be an oversimplification due to the limited information in your question:
Assumption 1: This solution assumes that the getSunriseSunset method is synchronous, i.e. not dependent on any service or network call. If it is asynchronous, you would need a separate callback in your MainActivity (e.g. onSunriseRetrieved) to listen for when your data is returned, prior to updating your fragment.
Assumption 2: This solution also assumes that you want access to the fragment's views immediately after it has been added (hence, the executePendingTransactions call). That said, if you have FragmentB perform its own retrieval of the sunRise, then this assumption wouldn't be needed.
Define an interface in the Activity and :
public class MainActivity extends AppCompatActivity
{
public interface OnSunriseChangeListener
{
void onSunriseChange(String newText);
}
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null)
{
getFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, new FragmentB()))
.commit();
//assuming you want access to the fragment's views immediately
getFragmentManager().executePendingTransactions();
}
getSunriseAndUpdateFragment(fragment);
}
/**
* Assumes that getSunriseSunset is synchronous and assumes that FragmentB is created.
*/
private void getSunriseAndUpdateFragment()
{
String newSunRiseValue = getSunriseSunset();
((FragmentB)getFragmentManager().findFragmentById(R.id.fragment_b)).onSunriseChange(newSunRiseValue);
}
// other code not shown...
}
Have FragmentB implement the interface:
public class FragmentB extends Fragment implements OnSunriseChangeListener
{
#Override
public void onSunriseChange(String newSunriseData)
{
TextView textView = getView.findViewById(R.id.DisplaySunrise);
textView.setText(newSunriseText);
}
I've recently tried to use an interface for fragment-activity communication. The idea is that when a button is pressed in a fragment, it retrieves data from an EditText in the same fragment, it then sends the string to the MainActivty - this controls all my fragments - which then starts another fragment and delivers the string to this fragment for use later, however, I'm having trouble initially setting up the first interface which sends the data. Unfortunately nothing happens, and I cannot therefore get to the next fragment which should be displayed. Additionally I have tried using getActivity() but it cannot find the associated method within the fragment, leading me to believe that the fragments somehow aren't directly connected to MainActivity (I've only just grasped basics of Java and a little of Android, just learning.)
I've listed the relevant information below, thanks for the assistance!
Fragment
public class CreateWorkoutFragment extends Fragment implements OnClickListener {
View rootViewCreateWorkoutFragment;
EditText editTextWorkoutName;
// Using an ImageView for custom button
ImageView buttonNext;
String valueCreateWorkoutEditText;
OnDataPass dataPasser;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
rootViewCreateWorkoutFragment = inflater.inflate(R.layout.fragment_create_workout, container, false);
buttonNext = (ImageView) rootViewCreateWorkoutFragment.findViewById(R.id.button_workout_name_next);
editTextWorkoutName = (EditText) rootViewCreateWorkoutFragment.findViewById(R.id.edit_text_workout_name);
buttonNext.setOnClickListener(this);
return rootViewCreateWorkoutFragment;
}
public interface OnDataPass {
public void onDataPass(String data);
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
dataPasser = (OnDataPass) activity;
}
public void passData(String data) {
dataPasser.onDataPass(data);
}
#Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button_workout_name_next:
valueCreateWorkoutEditText = editTextWorkoutName.getText().toString();
passData(valueCreateWorkoutEditText);
break;
}
}
}
Main Activity
public class MainActivity extends FragmentActivity implements OnClickListener, CreateWorkoutFragment.OnDataPass {
ImageView buttonCreate;
Fragment newFragment;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(R.style.AppThemeBlue);
setContentView(R.layout.activity_main);
buttonCreate = (ImageView) findViewById(R.id.create_foreground);
buttonCreate.setOnClickListener(this);
FragmentManager fragManager = getSupportFragmentManager();
FragmentTransaction tranManager = fragManager.beginTransaction();
CreateWorkoutFragment createWorkoutFrag = new CreateWorkoutFragment();
// fragment_change is just the area in XML where fragments switch
tranManager.add(R.id.fragment_change, createWorkoutFrag);
tranManager.commit();
newFragment = null;
}
#Override
public void onDataPass(String data) {
// CreateFragment is not to be confused with CreateWorkoutFragment
// CreateFragment is the fragment I'm trying to start when any strings
// are obtained from CreateWorkoutFragment
newFragment = new CreateFragment();
}
#Override
public void onClick(View v) {
switch (v.getId()) {
// create_foreground is just an ImageView used as a button
// Additionaly, other buttons are used to create other fragments,
// I've cut them out currently as they are not nessesary which is
// why CreateWorkoutFragment is only button and default currently
case R.id.create_foreground:
newFragment = new CreateWorkoutFragment();
break;
default:
newFragment = new CreateWorkoutFragment();
}
FragmentTransaction tranManager = getSupportFragmentManager().beginTransaction();
tranManager.replace(R.id.fragment_change, newFragment);
tranManager.addToBackStack(null);
tranManager.commit();
}
}
Sorry the code isn't exactly tidy, however, it was the most relevant code cut out from a large class. As I said, I have tried other methods yet cannot get any response from MainActivity either way. Thanks in advance!
Just before I posted: Got the app to write logcat messages to me, it manages to pass the data when the button is clicked - at least I think, and is something to do with the fragment not starting! At MainActivity>onDataPass()>new Fragment = new CreateFragment() Any ideas? As mentioned before, other buttons do exist and manage to change the fragment. However, were cutout to reduce amount of code posted.
getActivity() but it cannot find the associated method within the fragment
This is because getActivity() returns an Activity, not a MainActivity which is your custom subclass. You can easily fix this with a cast. For example, in your fragment, you can do this:
OnDataPass main = (OnDataPass) getActivity();
main.onDataPass(message);
Since such a cast is required, the interface seems to get in the way in my opinion. You can just as easily cast directly to MainActivity:
MainActivity main = (MainActivity) getActivity();
main.onDataPass(message);
What I ought to do is change the view/layout of Fragment without creating another class for fragment on click of a button.
For example I have an activity - ContactsActivity and I have a fragment - ContactsFragment.
The Standard way of using Fragments:
From ContactsActivity I call ContactsFragment by -
getFragmentManager().beginTransaction().replace(android.R.id.content, new ContactsFragment())
.commit();
Code for setting View in ContactsFragment class -
#Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
rootView = inflater.inflate(R.layout.contacts_primary, container, false);
return rootView;
}
**Now comes how I do what I want to do ** (Change the view of fragment)
I change only the view of ContactsFragment by doing a bad kind of hack.
I change the onCreateView() shown above to this -
#Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
//Set the view to R.layout.contacts_primary
rootView = inflater.inflate(R.layout.contacts_primary, container, false);
//Set the view to R.layout.contacts_secondary
if(getActivity().getIntent()!=null && getActivity().getIntent().getBooleanExtra("s", false)) {
rootView = inflater.inflate(R.layout.contacts_secondary, container, false);
Log.e(tag,getActivity().getIntent().getExtras().toString());
return rootView;
}
//This is the onClickListener which again calls the ContactsActivity class,
//this time with an Intent which I used above to change the view from
//R.layout.contacts_primary to R.layout.contacts_secondary
Button button = (Button)rootView.findViewById(R.id.button1);
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
startActivity(new Intent(getActivity(), ContactsActivity.class).putExtra("s",true));
}
});
Now everything works as I want and flawlessly.
But I have a very strong feeling that either all of it is wrong and Fragments aren't supposed to work this way or I am using a hectic hack to achieve what can be done by few lines of code.
So please let me know what is it? And if there is a standard way of doing what I am trying to do.
For me passing additional argument on which base fragment decides wich layout to use seems totally ok. But there is cleaner way of doing what you want to achieve without starting another activity.
First of all pass argument to fragment by making standard static new instance method in fragment (we cannot pass this argument in constructor as android always recreates fragments using empty constructor). Something like this:
public static ContactsFragment newInstance(boolan firstView) {
ContactsFragment fragment = new ContactsFragment();
Bundle args = new Bundle();
args.putBoolean("yourArg", firstView);
fragment.setArguments(args);
return fragment;
}
Every time you have to initiate your fragment do this with this method.
Then declare interface in your fragment to communicate with your activity. Like this
public interface NewViewListener {
public void showNewView(boolen firstView);
}
Than make your activity implement it so your activity han a method where it can place new fragment in container view. In your fragments onAttach and onDetach methodsmake sure your activity implements this interface and hold reference to your activity in private NewViewListener field in your fragment. Like this:
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (NewViewListener ) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement NewViewListener ");
}
}
#Override
public void onDetach() {
super.onDetach();
mListener = null;
}
Then in on button click method call showNewView method on your activity with whatever argument you want indicating which view you want in new fragment instance. And in your activity method showNewVew fragment in the container. Like this:
#Override
public void showNewView(boolean firstView) {
getFragmentManager().beginTransaction().replace(android.R.id.content, ContactsFragment.newInstance(firstView)
.commit();
}
In your fragments onCreateView you may get passed arguments and decide which view you want to use.
i have code like this
public class fragment2 extends Fragment{
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.content, container, false);
}
}
i want to call activity class instead of layout.
please help me.
Following link talks about communication between two fragment via activity..
http://developer.android.com/training/basics/fragments/communicating.html
same example can be used to communicate back to the activity from fragment.
For example if you have activity and fragment called MyActivity and MyFragment,
you can provide some public method in MyActivity. After this in the MyFragment#onCreateView()
you can call
String layout = ((MyActivity)getActivity()).getMyLayoutLink()
You could create an interface in the fragment, then implement it in the activity. You then set the listener in the fragments onAttach() method. This is called before onCreateView(). This pattern makes your code reusable and this technique can apply for all your activities and fragments they use. Example code for the fragment:
public class YourFragment extends Fragment {
private OnItemSelectedListener onClickListener;
public interface OnItemSelectedListener {
//implement and use this function in your calling activity
public void yourItemSelected(String link);
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
//check that the activity implements the interface
if (activity instanceof OnItemSelectedListener) {
//set the listener to the calling activity
onClickListener = (OnItemSelectedListener) activity;
} else {
throw new ClassCastException(activity.toString()
+ " must implement YourFragment.OnItemSelectedListener");
}
}
}
For more info see the android documentation:
http://developer.android.com/training/basics/fragments/communicating.html
http://developer.android.com/guide/components/fragments.html#Lifecycle