Code translations from activity to fragment - android

I'm making a conversion from 2 clases that extends ListActivity to extend ListFragment due to code proposals.
As I know, the Fragment is related to the Activity, so at first using getActivity()... should do the work to adapt most methods. Other times, I've defined Activity activity_context; and I use this.
Anyway, I have some functions that I'm not able to adapt, and I would need some help.
The first is: RecentCallsListActivity extends Fragment
public class RecentCallsListActivity extends ListFragment
...
private static final class QueryHandler extends AsyncQueryHandler {
private final WeakReference<RecentCallsListActivity> mActivity;
...
public QueryHandler(Context context) {
super(context.getContentResolver());
mActivity = new WeakReference<RecentCallsListActivity>(
(RecentCallsListActivity) context); //GETTING THE ERROR HERE
}
ERROR: Cannot cast from context to RecenCallsListActivity
public void onActivityCreated(Bundle state) {
super.onActivityCreated(state);
mQueryHandler = new QueryHandler(activity_context);
The second is: CallDetailActivity extends Fragment
public class CallDetailActivity extends ListFragment
...
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_CALL: {
// Make sure phone isn't already busy before starting direct call
TelephonyManager tm = (TelephonyManager)
getActivity().getSystemService(Context.TELEPHONY_SERVICE);
if (tm.getCallState() == TelephonyManager.CALL_STATE_IDLE) {
Intent callIntent = new Intent(Intent.ACTION_CALL,
Uri.fromParts("tel", mNumber, null));
startActivity(callIntent);
return true;
}
}
}
return super.onKeyDown(keyCode, event); //GETTING IT HERE
}
ERROR: The method onkeyDown(int, keyevent) is undefined for the type ListFragment

QueryHandler
At runtime you're providing a class that is not a RecentCallsListActivity or extend from it. The way of handling this is to define an interface that exposes an API that should be implemented by parent activity. If you have multiple activities that need to implement this interface and have the same implementation, you can make a super Activity that implements your interface and each of your activity will extend from this super class. But if you have a single class don't need to do this.
onKeyDown handling - as you can see from API, the fragment doesn't expose any onKeyDown. I have some ideas why this was not implemented, but you can delegate this action from activity to fragment, so that if the fragment is not present and it doesn't wish to consume the event, then you can call activity's super.onKeyDown.
Maybe some code will provide some light and will be helpful.
Sample fragment class:
public class QueryFragment extends Fragment {
public static interface RecentCallsLister {
public void someAction();
}
private RecentCallsLister recentCallsListener;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (activity instanceof RecentCallsLister) {
this.recentCallsListener = (RecentCallsLister) activity;
} else {
throw new ClassCastException("Parent class does not implement RecentCallsLister");
}
}
#Override
public void onDetach() {
this.recentCallsListener = null;
super.onDetach();
}
public boolean manageOnKeyDown(int keyCode, KeyEvent event) {
if(keyCode == KeyEvent.KEYCODE_CALL) {
// your specific code
return true;
}
return false;
}
}
Sample parent activity class:
public class QueryParentActivity extends FragmentActivity implements RecentCallsLister {
private static final String QUERY_FRAGMENT_TAG = "QUERY_FRAGMENT_TAG";
protected void addQueryFragment() {
QueryFragment fragment = new QueryFragment();
getSupportFragmentManager().beginTransaction()
.add(R.id.where_do_want_to_have_me, fragment, QUERY_FRAGMENT_TAG).commit();
}
#Override
public void someAction() {
}
#Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (queryFragmentConsumedKeyDown(keyCode, event)) {
return true;
}
return super.onKeyDown(keyCode, event);
}
private boolean queryFragmentConsumedKeyDown(int keyCode, KeyEvent event) {
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentByTag(QUERY_FRAGMENT_TAG);
if (fragment != null) {
return ((QueryFragment) fragment).manageOnKeyDown(keyCode, event);
}
return false;
}
}
EDIT for your first issue:
replace the QueryHandler constructor from:
public QueryHandler(Context context) {
super(context.getContentResolver());
mActivity = new WeakReference<RecentCallsListActivity>((RecentCallsListActivity) context);
}
to:
public QueryHandler(Context context, RecentCallsListActivity fragmentInstance) {
super(context.getContentResolver());
mActivity = new WeakReference<RecentCallsListActivity>(fragmentInstance);
}
Instantiate it as: mQueryHandler = new QueryHandler(activity_context, this);

Related

Unable to change the value of a variable of my fragment from an external class

I am working with fragments but I am not able to access its variables from an external class.
Currently, I have a fragment fragmentView which has a settings button. Whenever it is pressed, it shows an UI Element to define different settings. I copy the code that I have:
Fragment
public static Boolean show = false;
private void initSettingsPanel() {
m_settingsBtn = (ImageButton) m_activity.findViewById(R.id.settingButton);
/* click settings panel button to open or close setting panel. */
m_settingsBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
m_settingsLayout = (LinearLayout) m_activity.findViewById(R.id.settingsPanelLayout);
if (m_settingsLayout.getVisibility() == View.GONE) {
m_settingsLayout.setVisibility(View.VISIBLE);
if (m_settingsPanel == null) {
m_settingsPanel = new SettingsPanel(m_activity, show); //HERE I CALL THE EXTERNAL CLASS
}
} else {
m_settingsLayout.setVisibility(View.GONE);
}
}
});
}
SettingsPanel
private Activity m_activity;
private static Boolean p_show;
private Switch p_switch;
public SettingsPanel(Activity activity, Boolean show
) {
p_show = show;
m_activity = activity;
initUIElements(); // Init switch
}
private void initUIElements() {
p_switch = (Switch) m_activity.findViewById(R.id.showSwitch);
setUIListeners();
}
private void setUIListeners() {
p_switch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
p_show = isChecked;
}
});
}
}
What is currently happening is that when I activate the switch I change the value of th evariable of Pannel but it does not affect to the fragment. Why? Is there any other way to change the value of its variables not sending to SettingPanel each of the variables? Is it at least a correct way to do it?
You can register an interface between your activity and your fragment(s) -
public interface CallbackInterface {
//you can also return values from this interface
void performSomething(); //Add params to this method as per yourneed
}
In your activity class -
public YourActity extends AppCompactActivity implements CallbackInterface {
//your interface instance
private CallbackInterface mCallbackInterface;
#Override
void performSomething() {
// Write your code here.
}
}
}
In your fragment, you can override the
onAttach()
to register to the interface --
#Override
public void onAttach(Context context) {
super.onAttach(context);
mCallbackInterface = (CallbackInterface) context;
}
Voila! Now you can use this interface to send any data back to your activity.
In the end, I have created an abstract class in order to get and set the variables:
ShowVar
public abstract class ShowVar {
static private Boolean show = false;
public Boolean getShow() {
return show;
}
public void setShow(Boolean value) {
this.show = value;
}
}
From my SettingPanel I have instance ShowVar to set the new values of the variable each time and change the switch
SettingsPanel
public class SettingsPanel {
public ShowVar showVar = new ShowVar() {
#Override
public void set_Show(Boolean show) {
super.setShow(show);
}
};
}
and from my fragment I have access to the values by using my variable m_settingsPanel
Fragment
m_settingsPanel.showVar.getShow()

How to override an Activity's method in a Fragment

I want to override a method present in an Activity through a Fragment which is added in the activity.
I have following setup:
BaseActivity
public class BaseActivity extends AppCompatActivity {
// gets triggered whenever I set a toolbar title
// from an Activity or Fragment
public boolean shouldLimitToolbarCharacters () {
return true;
}
}
BaseFragmentActivity
public class BaseFragmentActivity extends BaseActivity implements ToolbarBehaviour {
// override from BaseActivity and from ToolbarBehaviour
#Override
public boolean shouldLimitToolbarCharacters () {
// instead send it to fragment somehow
return true;
}
}
ToolbarBehaviour
// implemented by BaseFragmentActivity above
interface ToolbarBehaviour {
boolean shouldLimitToolbarCharacters();
}
BaseFragment
public class BaseFragment extends Fragment {
#Override
public void onAttach (Context context) {
if (context instanceof ToolbarBehaviour) {
ToolbarBehaviour toolbarBehaviour = (ToolbarBehaviour) context;
// ?? now what
}
}
}
How do I override BaseFragmentActivity's shouldLimitToolbarCharacters() from BaseFragment or its sub-fragments?
Whenever I trigger shouldLimitToolbarCharacters() from a sub-fragment of BaseFragment, it should use sub-fragment's provided overridden method for shouldLimitToolbarCharacters().
Thanks.
Call a method of fragment inside Activity as in
BaseFragmentActivity
public class BaseFragmentActivity extends BaseActivity implements ToolbarBehaviour {
BaseFragment fragment; // init in oncreate or something;
// override from BaseActivity and from ToolbarBehaviour
#Override
public boolean shouldLimitToolbarCharacters () {
if(fragment == null)
return false;
return fragment.shouldLimitToolbarCharacters();
}
}
BaseFragment
public class BaseFragment extends Fragment {
public boolean shouldLimitToolbarCharacters(){
return true; // do what ever you want to do
}
}
you can directly access the activity method through ((BaseFragmentActivity )getActivity()).shouldLimitToolbarCharacters();
and use like this
#Override
public void onAttach (Context context) {
if (context instanceof BaseFragmentActivity) {
((BaseFragmentActivity)getActivity()).shouldLimitToolbarCharacters();
}
}

Android unit testing fragment with roboletric in MPV application

I am reinventing my app using a classic MVP approach. In order to to this I read many many articles and tutorials, and what I came out with is that the best way is to :
create an interface for the presenter and one for the view
make fragments and activities implements view interfaces
create an implementation of the presenter interface, which takes in the constructor an instance of the the view it manages, and hold a reference to the presenter inside the view's implementation.
So I have created this classes
VIEW INTERFACE
public interface SignupEmailView extends BaseView {
void fillEmail(String email);
void onEmailInvalid(String error);
void onDataValidated();
}
PRESENTER INTERFACE
public interface SignupEmailPresenter {
void initData(Bundle bundle);
void validateData(String email);
}
VIEW IMPLEMENTATION
public class FrSignup_email extends BaseSignupFragmentMVP implements IBackHandler, SignupEmailView {
public static String PARAM_EMAIL = "param_email";
#Bind(R.id.signup_step2_new_scrollview)
ScrollView mScrollview;
#Bind(R.id.signup_step2_new_lblTitle)
SuperLabel mLblTitle;
#Bind(R.id.signup_step2_new_lblSubtitle)
TextView mLblSubtitle;
#Bind(R.id.signup_step2_new_txtEmail)
EditText mTxtEmail;
#Bind(R.id.signup_step2_new_btnNext)
Button mBtnNext;
protected SignupActivityView mActivity;
SignupEmailPresenter mPresenter;
public FrSignup_email() {
// Required empty public constructor
}
public static FrSignup_email newInstance(String email) {
FrSignup_email fragment = new FrSignup_email();
Bundle b = new Bundle();
b.putString(PARAM_EMAIL, email);
fragment.setArguments(b);
return fragment;
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mActivity = (SignupActivityView) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement IResetPasswordBridge");
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = loadView(inflater, container, savedInstanceState, R.layout.fragment_signup_email);
mPresenter = new SignupEmailPresenterImpl(this);
ButterKnife.bind(this, view);
return view;
}
#Override
public final void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
applyCircularReveal();
mPresenter.initData(this.getArguments());
mTxtEmail.setImeOptions(EditorInfo.IME_ACTION_NEXT);
mTxtEmail.setOnEditorActionListener(new TextView.OnEditorActionListener() {
#Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_NEXT) {
mPresenter.validateData(mTxtEmail.getText().toString());
return true;
}
return false;
}
});
mTxtEmail.setOnTouchListener(new OnTouchCompoundDrawableListener_NEW(mTxtEmail, new OnTouchCompoundDrawableListener_NEW.OnTouchCompoundDrawable() {
#Override
public void onTouch() {
mTxtEmail.setText("");
}
}));
mBtnNext.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mPresenter.validateData(mTxtEmail.getText().toString());
}
});
}
#Override
public void fillEmail(String email) {
mTxtEmail.setText(email);
}
#Override
public void onEmailInvalid(String error) {
displayError(error);
}
#Override
public void onDataValidated() {
changeFieldToValid(mTxtEmail);
setEmail(mTxtEmail.getText().toString());
// the activity shows the next fragment
mActivity.onEmailValidated();
}
#Override
public boolean doBack() {
if (!isLoading()) {
mActivity.onEmailBack();
}
return true;
}
#Override
public void displayError(String error) {
changeFieldToInvalid(mTxtEmail);
mLblSubtitle.setText(error);
mLblSubtitle.setTextColor(ContextCompat.getColor(getActivity(), R.color.field_error));
}
}
PRESENTER IMPLEMENTATION
public class SignupEmailPresenterImpl implements SignupEmailPresenter {
private SignupEmailView mView;
public SignupEmailPresenterImpl(SignupEmailView view) {
mView = view;
}
#Override
public void initData(Bundle bundle) {
if (bundle != null) {
mView.fillEmail(bundle.getString(FrSignup_email.PARAM_EMAIL));
}
}
#Override
public void validateData(String password) {
ValidationUtils_NEW.EmailStatus status = ValidationUtils_NEW.validateEmail(password);
if (status != ValidationUtils_NEW.EmailStatus.VALID) {
mView.onEmailInvalid(ValidationUtils_NEW.getEmailErrorMessage(status));
} else {
mView.onDataValidated();
}
}
}
Now the fragment is held by an activity which implements this view interface and has its own presenter
public interface SignupActivityView extends BaseView {
void onEmailValidated();
void onPhoneNumberValidated();
void onPasswordValidated();
void onUnlockCodeValidated();
void onResendCodeClick();
void onEmailBack();
void onPhoneNumberBack();
void onPasswordBack();
void onConfirmCodeBack();
void onSignupRequestSuccess(boolean resendingCode);
void onSignupRequestFailed(String errorMessage);
void onTokenCreationFailed();
void onUnlockSuccess();
void onUnlockError(String errorMessage);
void showTermsAndConditions();
void hideTermsAndConditions();
}
My idea is to have a unit test for each project unit, so for each view and presenter implementation I want a unit test, so I want to unit test my fragment with roboletric, and for example I want to test that if I click the "NEXT" button and the email is correct, the hosting Activity's onEmailValidated()method is called. This is my test class
public class SignupEmailViewTest {
private SignupActivity_NEW mActivity;
private SignupActivity_NEW mSpyActivity;
private FrSignup_email mFragment;
private FrSignup_email mSpyFragment;
private Context mContext;
#Before
public void setUp() {
final Context context = RuntimeEnvironment.application.getApplicationContext();
this.mContext = context;
mActivity = Robolectric.buildActivity(SignupActivity_NEW.class).create().visible().get();
mSpyActivity = spy(mActivity);
mFragment = FrSignup_email.newInstance("");
mSpyFragment =spy(mFragment);
mSpyActivity.getFragmentManager()
.beginTransaction()
.replace(R.id.signupNew_fragmentHolder, mSpyFragment)
.commit();
mSpyActivity.getFragmentManager().executePendingTransactions();
}
#Test
public void testEmailValidation() {
assertTrue(mSpyActivity.findViewById(R.id.signup_step2_new_lblTitle).isShown());
assertTrue(mSpyActivity.findViewById(R.id.signup_step2_new_lblSubtitle).isShown());
mSpyActivity.findViewById(R.id.signup_step2_new_btnNext).performClick();
assertTrue(((SuperLabel) mSpyActivity.findViewById(R.id.signup_step2_new_lblSubtitle)).getText().equals(mContext.getString(R.string.email_empty)));
((EditText) mSpyActivity.findViewById(R.id.signup_step2_new_txtEmail)).setText("aaa#bbb.ccc");
mSpyActivity.findViewById(R.id.signup_step2_new_btnNext).performClick();
verify(mSpyFragment).onDataValidated();
verify(mSpyActivity).onEmailValidated();
}
}
everything works well, is just the last verify which doesn't work. Note that the previous verify works, so onEmailValidated is called for sure.
Aside from this specific case, I have some point to discuss:
If with roboeletric I am forced to use an activity to instantiate a fragment, how can I test the fragment in complete isolation (which would be the unit tests goal)? I mean, if I use Robolectric.setupActivity(MyActivity.class) and the activity instantiates somewhere a fragment, it will load the activity and the fragment, which is good, but what if the activity manages a flow of fragments? How can I test the second or third fragment without manually navigating to it? Someone can say to use a dummy activity and use FragmentTestUtil.startFragment, but what in the fragment's onAttach() method is implemented the bridging with the parent activity? Is it me going on the wrong way or are this problems still unsolved?
thanks
Actually you don't even require Roboelectric to do any of those tests.
If each fragment/activity implements a different view interface you could implement fake views and instantiate those instead of the activity/fragment. In this way you could have isolated tests.
If you don't want to implement all the methods of the view interface you could use Mockito and stub only the ones that your unit test requires.
Let me know if you need sample code.

Using Espresso idling resource with multiple activities

I have a firstActivity that launches the secondActivity, where in the secondActivity I have a loading Dialog (not AsyncTask), and I need to make Espresso wait until the dialog disappears before it continues with the test.
Where do I have to implement the IdlingResource? How can I make it wait for the dismissDialog() function?
Here is what I've tried to do:
class DocumentLoadingIdlingResource implements IdlingResource {
private ResourceCallback callback;
#Override
public String getName() {
return "Documnet loading idling resource";
}
#Override
public boolean isIdleNow() {
Activity activity;
try {
activity = getCurrentActivity();
} catch (Throwable e) {
return false;
}
if(activity.getClass().getName().equals(EditorActivity.class.getName())
&& activity.loadingDialogShowing() == false) {
return false;
}
return true;
}
#Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
this.callback = callback;
}
}
Activity getCurrentActivity() throws Throwable {
getInstrumentation().waitForIdleSync();
final Activity[] activity = new Activity[1];
runTestOnUiThread(new Runnable() {
#Override
public void run() {
java.util.Collection<Activity> activites = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);
activity[0] = com.google.common.collect.Iterables.getOnlyElement(activites);
}});
return activity[0];
}
This class is implemented in the test class.
There are a few problems here:
Your isIdleNow() calls getCurrentActivity() which calls waitForIdleSync() and runTestOnUiThread(). isIdleNow Javadoc says: "Espresso will always call this method from the main thread, therefore it should be non-blocking and return immediately." So this won't work as is, but you could call getActivitiesInStage directly from isIdleNow.
Your other issue is that you store the reference to ResourceCallback but never invoke onTransitionToIdle, also you should allow for the possibility of more than one ResourceCallback being registered and call onTransitionToIdle on all of the callbacks.
You can do the following:
Copy/Paste IdlingResource into your app as com.mycompany.IdlingResource.
Then have your Activity implement that interface and make sure to call onTransitionToIdle when the dialog goes away and make sure isIdleNow returns false iff the dialog is showing.
In your test code, write a "IdlingResourceAdapter" that wraps com.mycompany.IdlingResource and turns it into an Espresso IdlingResource and register that with Espresso.
This will be simpler once this issue is implemented: https://code.google.com/p/android-test-kit/issues/detail?id=71
I stumbled upon this question in my search for a similar answer. Using concepts from Stefano Dacchille's article on IdlingResources, I built the following idling resource that waits for a specific Activity to be active before firing. In my case, I know the dialog is showing when a fragment with a specific tag exists. This isn't the same as the OP's test, but the concepts should translate well.
public class BusyWhenFragmentExistsInActivityIdlingResource implements IdlingResource {
private FragmentActivity activity = null;
private final String fragmentTag;
private ResourceCallback resourceCallback;
private boolean wasIdleLastTime = true; // Start off as idle
private final String name;
// Need this strong reference because ActivityLifecycleMonitorRegistry won't hold one
private final ActivityLifecycleCallback activityLifecycleCallback;
public BusyWhenFragmentExistsInActivityIdlingResource(
final Class<? extends FragmentActivity> clazz,
final String fragmentTag
){
name = BusyWhenFragmentExistsInActivityIdlingResource.class.getSimpleName()+" "+clazz.getSimpleName();
this.fragmentTag = fragmentTag;
activityLifecycleCallback = new ActivityLifecycleCallback() {
#Override
public void onActivityLifecycleChanged(Activity activity, Stage stage) {
if (!FragmentActivity.class.isAssignableFrom(activity.getClass())) {
return;
}
FragmentActivity fragmentActivity = (FragmentActivity) activity;
if (!clazz.isAssignableFrom(fragmentActivity.getClass())) {
return;
}
switch (stage){
case RESUMED:
BusyWhenFragmentExistsInActivityIdlingResource.this.activity = fragmentActivity;
break;
case STOPPED:
BusyWhenFragmentExistsInActivityIdlingResource.this.activity = null;
break;
}
}
};
ActivityLifecycleMonitorRegistry.getInstance()
.addLifecycleCallback(activityLifecycleCallback);
}
#Override
public String getName() {
return name;
}
#Override
public boolean isIdleNow() {
if (activity==null) {
return wasIdleLastTime = true;
}
boolean isIdleThisTime = activity
.getSupportFragmentManager()
.findFragmentByTag(fragmentTag)==null;
if (!wasIdleLastTime && isIdleThisTime && resourceCallback!=null){
resourceCallback.onTransitionToIdle();
}
return wasIdleLastTime = isIdleThisTime;
}
#Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
}
To use it, add something similar to this to your test:
#Before
public void setUp() throws Exception {
registerIdlingResources(new BusyWhenFragmentExistsInActivityIdlingResource(
SomeOtherActivity.class,
BaseActivity.LOADING_DIALOG
));
}

Adding Functionality to both Android's Activity (parent class) and FragmentActivity (child class) in best coding style (minimal duplication)

In Android Library, FragmentActivity extends Activity. I would like to add a few methods, and override some methods, of the original Activity.
import android.app.Activity
public class Activiti extends Activity {
public void myNewMethod() { ... }
}
Because of the original hierarchy, FragmentActivity extends Activity, myNewMethod() should also be present in my library's FragmentActiviti
import android.support.v4.app.FragmentActivity;
public abstract class FragmentActiviti extends FragmentActivity {
public void myNewMethod() { ... }
}
But this will lead to a duplication of code, which i do not want this happens. Is there a way to avoid this duplication?
Edit: Usage scenario
Activiti.java
public abstract class Activiti extends Activity {
private int current_orientation = Configuration.ORIENTATION_UNDEFINED; // ORIENTATION_UNDEFINED = 0
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
current_orientation = this.getResources().getConfiguration().orientation;
}
protected boolean isDevicePortrait() { return current_orientation == Configuration.ORIENTATION_PORTRAIT; }
}
FragmentActiviti.java
public abstract class FragmentActiviti extends FragmentActivity {
/* This onCreate() can be omitted. Just putting here explicitly. */
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
protected void someUtilsForFragments() { /* not used yet */ }
}
E_fragtest_06.java
public class E_fragtest_06 extends FragmentActiviti {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
System.out.printf(isDevicePortrait()); // this NOT WORK for now
}
}
Edit 2: Try using Util class
i think using the Decorator Class would be the most nicest way to solve this problem (no duplication of code). But the Decorator Pattern is just a bit hard (or impossible) to apply on Android Activity scenario.
i try implementing #hazzik's approach, but i still experience some problems.
ActivityUtil.java
public abstract class ActivityUtil {
private int current_orientation = Configuration.ORIENTATION_UNDEFINED; // ORIENTATION_UNDEFINED = 0
public void onCreate(Activity activity, Bundle savedInstanceState) {
activity.onCreate(savedInstanceState);
current_orientation = activity.getResources().getConfiguration().orientation;
}
public boolean isDevicePortrait() { return current_orientation == Configuration.ORIENTATION_PORTRAIT; }
}
Activiti.java
public class Activiti extends Activity {
private ActivityUtil activityUtil;
#Override
public void onCreate(Bundle savedInstanceState) {
activityUtil.onCreate(this, savedInstanceState);
}
protected boolean isDevicePortrait() { return activityUtil.isDevicePortrait(); }
}
FragmentActiviti.java
public abstract class FragmentActiviti extends FragmentActivity {
private ActivityUtil activityUtil;
#Override
public void onCreate(Bundle savedInstanceState) {
activityUtil.onCreate(this, savedInstanceState);
}
protected boolean isDevicePortrait() { return activityUtil.isDevicePortrait(); }
}
In ActivityUtil.onCreate(), activity.onCreate(savedInstanceState); is causing this compile error:
The method onCreate(Bundle) from the type Activity is not visible.
If i change Activity to Activiti:
public abstract class ActivityUtil {
public void onCreate(Activiti activity, Bundle savedInstanceState) { ... }
...
}
It will lead to another compile error in FragmentActiviti.onCreate()'s activityUtil.onCreate():
The method onCreate(Activiti, Bundle) in the type ActivityUtil is not applicable for the arguments (FragmentActiviti, Bundle)
i understand why those errors occur. But i just don't know how to avoid them.
To thanks all the guys who have been contributing to this question, especially #flup for guiding me about the Decorator Pattern, #hazzik and #donramos for your extensive efforts, i m here posting
My enhanced Android's Activity and FragmentActivity classes.
If you are also developing Android applications, i hope my codes could help you guys in some ways :-)
ActivityCore.java
package xxx.android;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.res.Configuration;
import android.os.Bundle;
public final class ActivityCore {
public interface ActivityCallbackInterface {
public void onCreateCallback(Bundle savedInstanceState);
public void onBeforeSaveInstanceState(Bundle outState);
public void onSaveInstanceStateCallback(Bundle outState);
}
private final Activity activity;
/**
* This current_orientation variable should be once set, never changed during the object life-cycle.
* But Activity.getResources() is not yet ready upon the object constructs.
* That's why THIS CLASS is wholly responsible to maintain THIS VARIABLE UNCHANGED.
*/
private int current_orientation = Configuration.ORIENTATION_UNDEFINED; // ORIENTATION_UNDEFINED = 0
public ActivityCore(Activity activity) { this.activity = activity; }
public void onCreate(Bundle savedInstanceState) {
((ActivityCallbackInterface) activity).onCreateCallback(savedInstanceState);
current_orientation = activity.getResources().getConfiguration().orientation;
}
public void onSaveInstanceState(Bundle outState) {
/**
* THIS is the best ever place i have found, to unload unwanted Fragments,
* thus prevent re-creating of un-needed Fragments in the next state of Activity.
* (state e.g. Portrait-to-Landscape, or Landscape-to-Portrait)
*
* The KEY is to do it BEFORE super.onSaveInstanceState()
* (my guess for this reason is, in super.onSaveInstanceState(),
* it saves the layout hierarchy, thus saved the Fragments into the Bundle also.
* Thus restored.
* Note that Fragments NOT IN LAYOUT, having ONLY TAGS, are also restored.)
*/
((ActivityCallbackInterface) activity).onBeforeSaveInstanceState(outState);
((ActivityCallbackInterface) activity).onSaveInstanceStateCallback(outState);
}
public int getCurrentOrientation() { return current_orientation; }
public boolean isDevicePortrait() { return current_orientation == Configuration.ORIENTATION_PORTRAIT; }
public boolean isDeviceLandscape() { return current_orientation == Configuration.ORIENTATION_LANDSCAPE; }
public boolean isNewDevicePortrait() { return activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; }
public boolean isNewDeviceLandscape() { return activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; }
public boolean isPortrait2Landscape() { return isDevicePortrait() && isNewDeviceLandscape(); }
public boolean isLandscape2Portrait() { return isDeviceLandscape() && isNewDevicePortrait(); }
public String describeCurrentOrientation() { return describeOrientation(current_orientation); }
public String getCurrentOrientationTag() { return getOrientationTag(current_orientation); }
public String describeNewOrientation() { return describeOrientation(activity.getResources().getConfiguration().orientation); }
public String getNewOrientationTag() { return getOrientationTag(activity.getResources().getConfiguration().orientation); }
private String describeOrientation(final int orientation) {
switch (orientation) {
case Configuration.ORIENTATION_UNDEFINED: return "ORIENTATION_UNDEFINED"; // 0
case Configuration.ORIENTATION_PORTRAIT: return "ORIENTATION_PORTRAIT"; // 1
case Configuration.ORIENTATION_LANDSCAPE: return "ORIENTATION_LANDSCAPE"; // 2
case Configuration.ORIENTATION_SQUARE: return "ORIENTATION_SQUARE"; // 3
default: return null;
}
}
#SuppressLint("DefaultLocale")
private String getOrientationTag(final int orientation) {
return String.format("[%d:%s]", orientation, describeOrientation(orientation).substring(12, 16).toLowerCase());
}
}
Activity.java
package xxx.android.app;
import xxx.android.ActivityCore;
import xxx.android.ActivityCore.ActivityCallbackInterface;
import android.os.Bundle;
public abstract class Activity extends android.app.Activity implements ActivityCallbackInterface {
private final ActivityCore activityCore;
public Activity() { super(); activityCore = new ActivityCore(this); }
#Override
protected void onCreate(Bundle savedInstanceState) { activityCore.onCreate(savedInstanceState); }
#Override public void onCreateCallback(Bundle savedInstanceState) { super.onCreate(savedInstanceState); }
#Override
public void onBeforeSaveInstanceState(Bundle outState) {} // Optionally: let child class override
#Override
protected void onSaveInstanceState(Bundle outState) { activityCore.onSaveInstanceState(outState); }
#Override public void onSaveInstanceStateCallback(Bundle outState) { super.onSaveInstanceState(outState); }
public final int getCurrentOrientation() { return activityCore.getCurrentOrientation(); }
public final boolean isDevicePortrait() { return activityCore.isDevicePortrait(); }
public final boolean isDeviceLandscape() { return activityCore.isDeviceLandscape(); }
public final boolean isNewDevicePortrait() { return activityCore.isNewDevicePortrait(); }
public final boolean isNewDeviceLandscape() { return activityCore.isNewDeviceLandscape(); }
public final boolean isPortrait2Landscape() { return activityCore.isPortrait2Landscape(); }
public final boolean isLandscape2Portrait() { return activityCore.isLandscape2Portrait(); }
public final String describeCurrentOrientation() { return activityCore.describeCurrentOrientation(); }
public final String getCurrentOrientationTag() { return activityCore.getCurrentOrientationTag(); }
public final String describeNewOrientation() { return activityCore.describeNewOrientation(); }
public final String getNewOrientationTag() { return activityCore.getNewOrientationTag(); }
}
FragmentActivity.java
package xxx.android.support.v4.app;
import xxx.android.ActivityCore;
import xxx.android.ActivityCore.ActivityCallbackInterface;
import android.os.Bundle;
public abstract class FragmentActivity extends android.support.v4.app.FragmentActivity implements ActivityCallbackInterface {
private final ActivityCore activityCore;
public FragmentActivity() { super(); activityCore = new ActivityCore(this); }
#Override
protected void onCreate(Bundle savedInstanceState) { activityCore.onCreate(savedInstanceState); }
#Override public void onCreateCallback(Bundle savedInstanceState) { super.onCreate(savedInstanceState); }
#Override
public void onBeforeSaveInstanceState(Bundle outState) {} // Optionally: let child class override
#Override
protected void onSaveInstanceState(Bundle outState) { activityCore.onSaveInstanceState(outState); }
#Override public void onSaveInstanceStateCallback(Bundle outState) { super.onSaveInstanceState(outState); }
public final int getCurrentOrientation() { return activityCore.getCurrentOrientation(); }
public final boolean isDevicePortrait() { return activityCore.isDevicePortrait(); }
public final boolean isDeviceLandscape() { return activityCore.isDeviceLandscape(); }
public final boolean isNewDevicePortrait() { return activityCore.isNewDevicePortrait(); }
public final boolean isNewDeviceLandscape() { return activityCore.isNewDeviceLandscape(); }
public final boolean isPortrait2Landscape() { return activityCore.isPortrait2Landscape(); }
public final boolean isLandscape2Portrait() { return activityCore.isLandscape2Portrait(); }
public final String describeCurrentOrientation() { return activityCore.describeCurrentOrientation(); }
public final String getCurrentOrientationTag() { return activityCore.getCurrentOrientationTag(); }
public final String describeNewOrientation() { return activityCore.describeNewOrientation(); }
public final String getNewOrientationTag() { return activityCore.getNewOrientationTag(); }
}
Lastly, i really have to thanks you guys are being so so so helpful and keep updating the solving progress with me! You all are the key persons who make stackoverflow a perfect site for programmers. Should you spot any problems in my codes, or any rooms for improvements, please do not hesitate to help me again :-)
Some improvements?
It is because onBeforeSaveInstanceState() is implemented upon usage, all the three classes need to keep abstract. This leads to a duplication of the member variable current_orientation. If current_orientation could be put into class ActivityBase, or grouping it into somewhere else, it would be a lot nicer!
stupid me. i have fixed it :-)
For my point of view the best solution here is to delegate logic to some class, let's call it CustomActivityLogic.
Also you need to create common interface (CustomActivity) for your activities if you want to access some data or methods of activity classes from your logic class.
To call protected virtual overridden methods there are two solutions:
call method of supper from overridden method
make a new method in subclass and call super method from this new method. Call new method from shared logic.
CustomActivity.java
public interface CustomActivity {
void someMethod();
}
Activiti.java
import android.app.Activity
public class Activiti
extends Activity
implements CustomActivity {
private CustomActivityLogic logic = new CustomActivityLogic();
public void someMethod() { /***/ }
public void myNewMethod() { logic.myNewMethod(this); }
#Override
protected void onCreate(Bundle savedInstanceState) {
logic.onCreate(this, savedInstanceState); // call shared logic
super.onCreate(savedInstanceState); // call super
}
}
FragmentActivitii.java
import android.support.v4.app.FragmentActivity;
public class FragmentActivitii
extends FragmentActivity
implements CustomActivity {
private CustomActivityLogic logic = new CustomActivityLogic();
public void someMethod() { /***/ }
public void myNewMethod() { logic.myNewMethod(this); }
#Override
protected void onCreate(Bundle savedInstanceState) {
logic.onCreate(this, savedInstanceState); // call shared logic
super.onCreate(savedInstanceState); // call super
}
}
CustomActivityLogic.java
public class CustomActivityLogic {
public void myNewMethod(CustomActivity activity) { /*...*/ }
public void onCreate(Activity activity, Bundle savedInstanceState) {
/* shared creation logic */
}
}
Approach with making onCreate available to call from outside via CustomActivity interface
CustomActivity.java
public interface CustomActivity {
void someMethod();
void onCreateSuper(Bundle savedInstanceState);
}
Activiti.java
import android.app.Activity
public class Activiti
extends Activity
implements CustomActivity {
private CustomActivityLogic logic = new CustomActivityLogic();
public void someMethod() { /***/ }
public void myNewMethod() { logic.myNewMethod(this); }
#Override
protected void onCreate(Bundle savedInstanceState) {
logic.onCreate(this, savedInstanceState); // call shared logic
}
public void onCreateSuper(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // call super
}
}
FragmentActivitii.java
import android.support.v4.app.FragmentActivity;
public class FragmentActivitii
extends FragmentActivity
implements CustomActivity {
private CustomActivityLogic logic = new CustomActivityLogic();
public void someMethod() { /***/ }
public void myNewMethod() { logic.myNewMethod(this); }
#Override
protected void onCreate(Bundle savedInstanceState) {
logic.onCreate(this, savedInstanceState); // call shared logic
}
public void onCreateSuper(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // call super
}
}
CustomActivityLogic.java
public class CustomActivityLogic {
public void myNewMethod(CustomActivity activity) { /*...*/ }
public void onCreate(CustomActivity activity, Bundle savedInstanceState) {
/* shared creation logic */
activity.onCreateSuper(savedInstanceState); // call-back super
}
}
You wish to add helper methods that help keep track of the orientation. I'd think this is not quite big enough to warrant the creation of a subclass.
Put them in a helper class instead:
public class OrientationHelper {
private Activity activity;
private int current_orientation;
public OrientationHelper(Activity activity){
this.activity = activity;
orientation = Configuration.ORIENTATION_UNDEFINED;
}
public int getNewOrientation() {
return activity.getResources().getConfiguration().orientation;
}
// call this when you wish to update current_orientation
public void updateOrientation() {
current_orientation = getNewOrientation();
}
public int getCurrentOrientation() {
return current_orientation;
}
public boolean isDevicePortrait() {
return current_orientation == Configuration.ORIENTATION_PORTRAIT;
}
public boolean isDeviceLandscape() {
return current_orientation == Configuration.ORIENTATION_LANDSCAPE;
}
public boolean isNewDevicePortrait() {
return getCurrentOrientation() == Configuration.ORIENTATION_PORTRAIT;
}
public boolean isNewDeviceLandscape() {
return getCurrentOrientation() == Configuration.ORIENTATION_LANDSCAPE;
}
public boolean isPortrait2Landscape() {
return isDevicePortrait() && isNewDeviceLandscape();
}
public boolean isLandscape2Portrait() {
return isDeviceLandscape() && isNewDevicePortrait();
}
public String describeCurrentOrientation() {
return describeOrientation(current_orientation);
}
public String describeNewOrientation() {
return describeOrientation(getNewOrientation());
}
private String describeOrientation(int current_orientation) {
switch (current_orientation) {
case Configuration.ORIENTATION_UNDEFINED:
return "ORIENTATION_UNDEFINED";
case Configuration.ORIENTATION_PORTRAIT:
return "ORIENTATION_PORTRAIT";
case Configuration.ORIENTATION_LANDSCAPE:
return "ORIENTATION_LANDSCAPE";
case Configuration.ORIENTATION_SQUARE:
return "ORIENTATION_SQUARE";
default: return null;
}
}
}
In those activities that work with orientation (and only those), you can instantiate the OrientationHelper and call updateOrientation() in select places.
The other bit of code, that organizes the saving of the instance state, I would not put in a different class just so that you can reuse it. Because this is not where one would expect modifications to state saving to occur and therefore it might get overlooked. (It took me a bit of scrolling around to figure out what it's supposed to do.)
I think the most readable way to go about that is to write it out explicitly in each Activity where you use it.
One last thing to consider is that the Sherlock Actionbar already extends Activity. And rightly so, I think. But this means that you'll occasionally run into trouble if you extend Activity too.
How about using the Decorator Pattern? Unfortunately, this will require you to delegate all of the existing methods, or whichever ones are necessary for your purpose.
public class ActivityDecorator extends Activity
{
private Activity RealActivity;
public ActivityDecorator(Activity _realActivity)
{
RealActivity = _realActivity;
}
public void myNewMethod() { ... } // this exposes the added/new functionality
// unfortunately for old functionality you need to delegate
public void oldMethod() { RealActivity.oldMethod(); }
}
However, once you've done this once for the ActivityProxy class, you can construct instances of ActivityDecorator with types that derive Activity such as FragmentActivity in your case. E.g.
ActivityDecorator decorator = new ActivityDecorator(new FragmentActivity());
Your design problem is one of the issues addressed by the upcoming Java 8 virtual extensions. See URL below for more details:
http://java.dzone.com/articles/java-8-virtual-extension
In the meantime, there is no easy way. A decorator class will not work, instead implement a utility class that will be called by both of your classes:
EDITED BASED ON NEW INFO:
/** NOTE: cannot be abstract class **/
public class ActivitiBase {
private int current_orientation = Configuration.ORIENTATION_UNDEFINED; // ORIENTATION_UNDEFINED = 0
private Activity activity;
public void ActivitiBase(Activity activity) {
this.activity = activity;
}
public void onCreate(Bundle savedInstanceState) {
current_orientation = activity.getResources().getConfiguration().orientation;
}
public boolean isDevicePortrait() { return current_orientation ==
Configuration.ORIENTATION_PORTRAIT; }
}
public void myNewMethod() { ... }
}
Activiti class:
public class Activiti extends Activity {
private ActiviBase activitiBase;
public Activiti() {
activitiBase = new ActiviBase(this);
}
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activitiBase.onCreate(savedInstanceState);
}
public void myNewMethod() {
activitiBase.myNewMethod();
}
}
FrameActiviti class:
public class FrameActiviti extends FrameActivity {
private ActiviBase activitiBase;
public FrameActiviti() {
activitiBase = new ActiviBase(this);
}
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activitiBase.onCreate(savedInstanceState);
}
public void myNewMethod() {
activitiBase.myNewMethod();
}
}

Categories

Resources