Working with Robolectric , I'm very new to android. I made a first test class using Activity. It worked nicely.
Now I want make a test for fragment.
#RunWith(RobolectricTestRunner.class)
public class LoginFragmentTest {
private LoginFragment fragment;
#Before
public void setup() {
fragment = new LoginFragment();
startFragment(fragment);
assertThat(fragment, notNullValue());
assertThat(fragment.getActivity(), notNullValue());
}
private void startFragment(LoginFragment fragment) {
FragmentManager fragmentManager = new FragmentActivity().getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(fragment, null);
fragmentTransaction.commit();
}
#Test
public void login() {
EditText idEditText = (EditText)fragment.getActivity().findViewById(R.id.main_id);
assertThat(idEditText, notNullValue());
}
}
This is my first test class for Fragment class. It throws
"java.lang.IllegalStateException: Activity has been destroyed" in startFragment#fragmentTransaction.commit().
Anyone knows how to fix this ?
You can find whole source from https://github.com/msbaek/frame-test
Thanks in advance !!
In my case, specifically, my problem was when creating the activity.
I was using
activity = Robolectric.buildActivity(MyActivity.class).get();
And it should be
activity = Robolectric.buildActivity(MyActivity.class).create().get();
Hope it helps someone :D
#RunWith(RobolectricTestRunner.class)
public class LoginFragmentTest {
private LoginFragment fragment;
#Before
public void setup() {
fragment = new LoginFragment();
startFragment();
assertThat(fragment, notNullValue());
assertThat(fragment.getActivity(), notNullValue());
}
private void startFragment() {
FragmentActivity activity = new FragmentActivity();
shadowOf(activity).callOnCreate(null);
shadowOf(activity).callOnStart();
shadowOf(activity).callOnResume();
FragmentManager fragmentManager = activity.getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(fragment, null);
fragmentTransaction.commit();
}
#Test
public void login() {
EditText idEditText = (EditText) fragment.getView().findViewById(R.id.main_id);
assertThat(idEditText, notNullValue());
}
}
This is working version. Following 3 lines are important(it's from robolectric source - DialogFragmentTest).
shadowOf(activity).callOnCreate(null);
shadowOf(activity).callOnStart();
shadowOf(activity).callOnResume();
The fragments are supposed to be displayed from an Activity. The flow should be:
allocate a new fragment object in a FragmentActivity class
get the fragment manager to add the newly allocated fragment
In your case you do not have a connection to a real activity. You allocate a FragmentActivity with new FragmentActivity() and try to get the support manager. While this compiles there is no "real" activity able to manage your fragment. Fragments can be added on activities already displayed and here it's not the case.
I recommend reading this page as it explains these things very well: http://developer.android.com/guide/components/fragments.html
That happened for me when I used fragmentTransaction.commitAllowingStateLoss(); from sub Fragment whose parent fragment had setRetainInstance(true); I had activity as property what lead to leaking activity on rotation.
Related
I have a Main Activity with a bunch of Fragments connected to it.
One of the fragments has to be able to recieve data from MainActivity after it has been loaded in the FragmentTransaction and committed.
What is the best way to call a method in that specific fragment?
Do I have to implement a Interface and include it in the MainActivty just for this one fragment? Is there a better way? Can someone point me in the correct direction?
What I have tried now : (Failing at settings ContactsInterface in the MainActivity)
ContactsInterface
public interface ContactsInterface {
void notifyDenied();
void notifyGranted();
}
Fragment
Class.... implements ContactsInterface...
#Override
public void notifyDenied() {
Log.d("DENIED", "DENIED CALLBACK");
}
#Override
public void notifyGranted() {
Log.d("GRANTED", "GRANTED CALLBACK");
}
Main Activity
try {
contactsInterface = (ContactsInterface) this.getApplicationContext();
} catch (ClassCastException e) {
throw new ClassCastException(this.toString()
+ " Needs to implement the methods");
}
Last example throws an ClassCastException.
You need to cast the fragment itself not the Application Context.
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
ContactsInterface contactsInterface = (ContactsInterface) fragment;
//contactsInterface.notifyGranted();
//contactsInterface.notifyDenied();
You can get all the active fragments in the FragmentManager and filter for the ones that have the required interface:
supportFragmentManager.fragments
.map { it as? ContactsInterface }
.filterNotNull()
.forEach {
it.notifyDenied()
}
or if you need to use Java:
List<Fragment> fragments = getSupportFragmentManager().getFragments();
for(fragment : fragments) {
if(fragment instanceof ContactsInterface) {
((ContactsInterface)fragment).notifyDenied();
}
}
You may want to also fail (hard or soft) if no fragments are found.
The advantage of doing this is that you don't care how the fragment was started (manually or by layout) and also don't have to care about reconnecting when the activity restarts for example.
This library may be useful to you.
You should do something like this in your fragment:
#Override
public void onCreateView() {
super.onStart();
EventBus.getDefault().register(this);
}
#Override
public void onDestroyView() {
super.onStop();
EventBus.getDefault().unregister(this);
}
#Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(MessageEvent event) {/* Do something */};
MessageEvent is just an example, you can use whatever structure you want.
in the Activity you post messages like this:
EventBus.getDefault().post(new MessageEvent());
Check the documentation of the library to have a better idea and use a better approach to your case.
you can use onCreate() and onDestroy() instead. That depends on your logic and you you are updating the view of the fragment when you receive new messages from the Activity.
Happy coding :).
I read Android documentation on how to add a Fragment to an activty. It says that to add a Fragment to an Activty I should write this code inside the Activity class:
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
but this is for android.app.Fragment class.
For android.support.v4.Fragment, documentation say that instead of getFragmentManager() I should call getSupportFragmentManager() and that Activity must extends FragmentActivty.
So I did this change, and now this is my activity code:
public class ExampleActivity extends AppCompactActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragment);
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
}
}
AppCompactActivity is a subclass of FragmentActivity so I respected the constraints.
The problem is that if I run my application I get this error:
java.lang.RuntimeException:
com.myapplication.ExampleActivity#13388c6 must implement OnFragmentInteractionListener
I typed OnFragmentInteractionListener on Android doc web search and this is what I get:
Immediately below there is the link
https://developer.android.com/training/basics/fragments/communicating.html
that shows me a guide to communication between fragments. It speaks about ListFragment and I don't care it.
I'm very very confuse because every time I read documentation there are always things that force me to make internet search to find workaround or fix to problems.
Is there a persone that can explain me first how to fix this problem.
From the tutorial, you can read the following:
In order to receive event callbacks from the fragment, the activity
that hosts it must implement the interface defined in the fragment
class.
So, implement it in your Activity with something like this in your fragment:
public static class MainActivity extends Activity
implements HeadlinesFragment.OnHeadlineSelectedListener{
...
public void onArticleSelected(int position) {
// The user selected the headline of an article from the HeadlinesFragment
// Do something here to display that article
}
}
Why did you need it? Because you've forced the host activity to implement it with something like this:
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception
try {
mCallback = (OnHeadlineSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnHeadlineSelectedListener");
}
}
I am trying to prevent my DialogFragment opening twice. Here is what I do:
I try to keep only one instance of my fragment. I create and add my fragment like this:
//MyFragment.java
public static MyFragment mInstance;
public static void instantiateFragment() {
MyFragment myFragment = MyFragment.getInstance();
if(!myFragment.isAdded()) {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(myFragment, TAG);
ft.commit();
}
}
private static MyFragment getInstance() {
if(mInstance == null) {
mInstance = new MyFragment();
}
return mInstance;
}
And when a button is clicked, I intentionally try to add fragment twice like this:
MyFragment.instantiateFragment();
MyFragment.instantiateFragment();
But I get IllegalStateException: Fragment already added. Any ideas about that?
Thanks.
Indeed it's a problem with asynchronous commit of transactions, so as #Android jack stated you can use executePendingTransactions() like in this answer,
or even better use commitNow(),
or try something like this:
public static void instantiateFragment() {
Fragment myFragment = getSupportFragmentManager().findFragmentByTag(TAG);
if (myFragment == null) {
myFragment = MyFragment.getInstance();
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(myFragment, TAG);
ft.commit();
}
}
I think this has to do with the asynchronous behaviour of fragment transactions.Fragment Transactions are committed asynchronously. So at first call, your fragment is added but it is committed asynchronously.Again in your next call your fragment is not added as it is not committed yet so !myFragment.isAdded() returns false.Then while adding the fragment the previous transaction is committed due to which it raises exception.
Try to use this
getFragmentManager().executePendingTransactions();
before your (!myFragment.isAdded()) code.
Here is my main activity. I followed this guide about Fragments correctly. When I click "Back" button, my application is closed instead of returning to the MainScreenFragment. Why is this happening and why addToBackStack() doesn't work?
public class MainScreenActivity extends ActionBarActivity implements MainScreenFragment.OnFrameChoiced {
private MainScreenFragment mainScreenFragment;
private AddWordsFragment addWordsFragment;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_screen);
mainScreenFragment = new MainScreenFragment();
addWordsFragment = new AddWordsFragment();
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.container, mainScreenFragment).addToBackStack(null).commit();
}
#Override
public void choiceFrame(int id) {
switch (id) {
case R.id.add_new_words_frame:
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.container, addWordsFragment).addToBackStack(null).commit();
fm.executePendingTransactions();
break;
}
}
P.S. I tried to use a solution from this topic, but It still doesn't work.
did you try overriding the back like below:
#overide
public void onBackPressed(){
FragmentManager fm = getSupportFragmentManager();
if (fm.getBackStackEntryCount() > 1) {
fm.popBackStack();
} else {
finish();
}
}
(I know you have picked up your desirable answer, but I have found a little more against this problem)
Though as Android official site has documented:
By calling addToBackStack(), the replace transaction is saved to the back stack so the user can reverse the transaction and bring back the previous fragment by pressing the Backbutton.
But as a matter of fact, this is in a precondition that you are using the standard android activity, specifically, the android.app.Activity. Because this methods in android.app.Activity will work when Backbutton is pressed:
public void onBackPressed() {
if (mActionBar != null && mActionBar.collapseActionView()) {
return;
}
if (!mFragments.getFragmentManager().popBackStackImmediate()) {
finishAfterTransition();
}
}
But, if you are extending your xxxxActivity from someone else, for example, the AppCompatActivity, FragmentActivity, ActionBarActivity, it will be another story, because in FragmentActivity, onBackPressed() method is totally overrode:
public void onBackPressed() {
if (!mFragments.getSupportFragmentManager().popBackStackImmediate()) {
supportFinishAfterTransition();
}
}
Note that mFragments.getFragmentManager() is replaced by mFragments.getSupportFragmentManager(), so in cases like this, you should begin your FragmentTransaction using getSupportFragmentManager() of the Activity. and as a consequence of that, you don't have to override onBackPressed method in your Activity.
BTW, ActionBarActivity extends AppCompatActivity extends FragmentActivity,they all come from the support library, you know what I mean, remember to use getSupportFragmentManager() instead of getFragmentManager() when using support library in order to get the compatible behavior.
I know there is a Robolectric.shadowOf(Fragment) method and a ShadowFragment class, thought they aren't listed on the docs, but I can't make it work.
myFragment = new MyFragment();
myFragment.onCreateView(LayoutInflater.from(activity), (ViewGroup) activity.findViewById(R.id.container), null);
myFragment.onAttach(activity);
myFragment.onActivityCreated(null);
I'm working with API level 13 (Honeycomb).
Thanks.
Edit #4 & #5: In Robolectric 3.*, they split up the fragment starting functions.
For support fragments, you will need to add a dependency to your build.gradle:
testCompile "org.robolectric:shadows-supportv4:3.8"
Import: org.robolectric.shadows.support.v4.SupportFragmentTestUtil.startFragment;
For platform fragments, you don't need this dependency. Import: import static org.robolectric.util.FragmentTestUtil.startFragment;
They both use the same name of startFragment().
import static org.robolectric.shadows.support.v4.SupportFragmentTestUtil.startFragment;
#RunWith(RobolectricTestRunner.class)
#Config(constants = BuildConfig.class)
public class YourFragmentTest
{
#Test
public void shouldNotBeNull() throws Exception
{
YourFragment fragment = YourFragment.newInstance();
startFragment( fragment );
assertNotNull( fragment );
}
}
Edit #3: Robolectric 2.4 has an API for support and regular fragments. You can either use the newInstance() pattern or use the constructor when constructing your Fragment's.
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertNotNull;
import static org.robolectric.util.FragmentTestUtil.startFragment;
#RunWith(RobolectricGradleTestRunner.class)
public class YourFragmentTest
{
#Test
public void shouldNotBeNull() throws Exception
{
YourFragment fragment = new YourFragment();
startFragment( fragment );
assertNotNull( fragment );
}
}
Edit #2: There's a new helper if you're using support fragments (one that supports regular activities/fragments should be in the next release):
import static org.robolectric.util.FragmentTestUtil.startFragment;
#Before
public void setUp() throws Exception
{
fragment = YourFragment.newInstance();
startFragment( fragment );
}
Edit: If you upgraded to Robolectric 2.0:
public static void startFragment( Fragment fragment )
{
FragmentActivity activity = Robolectric.buildActivity( FragmentActivity.class )
.create()
.start()
.resume()
.get();
FragmentManager fragmentManager = activity.getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add( fragment, null );
fragmentTransaction.commit();
}
Original answer
As the other commenter suggested, you do need to use the fragment manager (instead of calling the lifecycle methods you listed above).
#RunWith(MyTestRunner.class)
public class YourFragmentTest
{
#Test
public void shouldNotBeNull() throws Exception
{
YourFragment yourFragment = new YourFragment();
startFragment( yourFragment );
assertNotNull( yourFragment );
}
I create a test runner and have a function that starts up a fragment for me so I can use it everywhere.
public class MyTestRunner extends RobolectricTestRunner
{
public MyTestRunner( Class<?> testClass ) throws InitializationError
{
super( testClass );
}
public static void startFragment( Fragment fragment )
{
FragmentManager fragmentManager = new FragmentActivity().getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add( fragment, null );
fragmentTransaction.commit();
}
}
You guys are all doing this the hard way. Just use FragmentTestUtil.
FragmentTestUtil.startFragment(yourfragment);
Support fragments have been moved to module:
shadows-support-v4
(as of July,2015, Robolectric v3.0)
Add a gradle dependency to app/build.gradle:
testCompile 'org.robolectric:shadows-support-v4:3.0'
Then import to your Robolectric test java class:
import org.robolectric.shadows.support.v4.SupportFragmentTestUtil;
Then you can start & use a support-v4 fragment for testing:
#Test
public void minimalFragmentTest() throws Exception {
MyFunFragment fragment = new MyFunFragment();
SupportFragmentTestUtil.startVisibleFragment(fragment);
assertThat(fragment.getView()).isNotNull();
}
References:
github changelog, moving support fragments to different module
Old android fragments are already deprecated, seems like support fragments soon will be deprecated too. To test androidx fragments you can use fragment scenarios with robolectric https://developer.android.com/training/basics/fragments/testing
testImplementation 'androidx.fragment:fragment-testing:1.2.2'
val scenario = launchFragmentInContainer<MyFragment>()
scenario.onFragment { fragment ->
assertNotNull(fragment.view.synteticInflatedView)
}
I'm pretty sure you have to create a FragmentTransaction using the FragmentManager, then it will work.
I just wanted to add that in Robolectric 2.0 even after doing:
activity = Robolectric.buildActivity(FragmentActivity.class).create().start().resume().get();
fragment.show(activity.getSupportFragmentManager(), null);
fragment.getDialog(); //This stills returns null
It still returned null for me. what I did was to add activity.getSupportFragmentManager().executePendingTransaction(); and it worked.
It seems robolectric doesn't run this for some reason. it seems that maybe the Looper is paused or something. any way this worked for me and it looks like this:
activity = Robolectric.buildActivity(FragmentActivity.class).create().start().resume().get();
fragment.show(activity.getSupportFragmentManager(), null);
activity.getSupportFragmentManager().executePendingTransactions();
fragment.getDialog();
SupportFragmentTestUtil.startFragment(fragment, AppCompatActivity::class.java)
If the activity is extending AppCompatActivity
This is using Kotlin