Android App Development Keeping it Simple

I get a lot of questions from “newbies” on how to best approach app development. I think the best way is to keep it simple, so this is my approach on app development, surely not the only one but it is one way to keep it simple.

Packages – Where to put things

Anyone that have worked with me knows that I have a really bad memory and never remembers the class name and where they are so when I code I try to keep my classes grouped by two simple rules.

Rule 1: Put the classes where they belong.

and if that fails…

Rule 2: Put them together with similar classes.

In an application we usually have more than one screen. A screen is an activity with all that it contains; views, models, fragments and other classes used by that activity.

So by rule 1 “put the class where they belong” I mean put the class within the same package as the rest of the classes used by that screen a.k.a where it belongs.

For example if I have an application with two different screens it would look something like this:

    com.jayway.myapp.screen.first.MyFirstActivity
    com.jayway.myapp.screen.first.MyFirstFragment
    com.jayway.myapp.screen.first.view.MyFirstView
    com.jayway.myapp.screen.first.MyFirstModel

    com.jayway.myapp.screen.second.MySecondActivity
    com.jayway.myapp.screen.second.MySecondFragment
    com.jayway.myapp.screen.second.view.MySecondView
    com.jayway.myapp.screen.second.MySecondModel

So the basic rule is one activity in each package. If we have a Master/Detail flow we will on tablet have both the Master and the Detail in the same screen, in that case I put my classes like this:

    com.jayway.myapp.screen.master.MasterActivity
    com.jayway.myapp.screen.master.MasterFragment

    com.jayway.myapp.screen.detail.DetailActivity
    com.jayway.myapp.screen.detail.DetailFragment

And on tablet the MasterActivity borrows the DetailFragment from the detail package.

Shared classes

If I have a class used on more than one screen I use the second rule and group them by what they are. For example if I have a view that appears in both of my screens I put it in a packages on the same level as the screen package. Like this:

    com.jayway.myapp.widget.MySharedView

If I have a general math class used by several screens I put it in:

    com.jayway.myapp.math.MyMath

Similar with all my other shared classes.

Other classes or grouping can be the domain model, this one should be kept in one place not within the screens. Networking is also another thing that might be good to keep for it self.

Following this rules and I get a codebase that is easier for my co-workers (and especially for me) to find the class needed to be changed or fixed without having to dig down in the code too much, we just need to look in the “screen”-package where the work needs to be done.

One other good thing about this is that if you should remove one screen, just delete the whole package and you are done. Try removing everything used by a screen if the classes are in several different packages spread all over the codebase, you sure will miss something.

The downside with this approach is that if you remove one screen for example the “second” we still have our “MySharedView” in a parent package even if it now only is used by one screen, but this really doesn’t matter because that view is probably quite general within the context of the app and can stay outside the screen.

It also keeps the tangle between packages in place, which is reason enough for me to chose this approach.

When developing a framework or a lib the first rule is usually to place the classes with similar classes just as the second rule, because this will make it easier for the user of the lib to find the classes they want to use.

Interface

Interfaces are good but usually overused making the code a bit harder to work with. For example a controller used in one screen is most likely never going to be used in any other screen so why make it replaceable by an interface? A network service on the other hand is a really good example on where an interface should be used for easy mocking and testing.

Activity

Instantiation

By adding the start method of activities as a static method in the activity itself we get an easy way start the activity and to avoid bloating the code with same code for setting up the activity in different places. It also makes it easier for other programmers to know what params are needed since they will be defined in the method. I prefer this before adding an activity router class with all the starting methods since a router class will add a lot of tangled code.

public class ItemDetailActivity extends Activity {
    
    private static final String EXTRA_ITEM_ID = "item_id";
    private static final String EXTRA_PATH    = "path";

    ...

    public static void start(Context context, int itemId, String path) {
        Intent launchIntent = new Intent(context, ItemDetailActivity.class);
        launchIntent.putExtra(EXTRA_ITEM_ID, itemId);
        launchIntent.putExtra(EXTRA_PATH, path);

        context.startActivity(launchIntent);
    }
}

In the example above we have an easy way to start the activity simply by:

ItemDetailActivity.start(context, 4, "/data/shared/");

Parameters

To read the params and prepare for screen rotation in an easy way we can parse the start bundle and the saved instance bundle in the same way. Like this:

protected void onCreate(Bundle savedInstanceState) {
    Bundle bundle = savedInstanceState == null ? getIntent().getExtras() : savedInstanceState
    mLabel = bundle.getString(EXTRA_LABEL, "not found");
    mCounter = bundle.getInt(EXTRA_COUNTER, 0);
}

If the ‘savedInstanceState’ parameter is null it means that the activity is started for the first time, if we for example rotate the screen ‘savedInstanceState’ will not be null.

To save the current state you can do it like this:

protected void onSaveInstanceState(Bundle outState) {
    // Put all the values you got from starting the activity.
    outState.putAll(getIntent().getExtras()); 

    // If some of them are changed you still need to save them manually.
    outState.putInt(EXTRA_COUNTER, mCounter);

    super.onSaveInstanceState(outState);
}

The first line means that we put all the starting params in the bundle so we don’t need to put every single value manually but only the one we changed.

Fragment

Instantiation

If you with fragment move the instantiation to the fragment just as we did with the activity we will get an easy instantiation of the fragment defined by the params in the method…

public static MyFragment newInstance(String label) {
    Bundle bundle = new Bundle();
    bundle.putString(EXTRA_LABEL, label);

    MyFragment fragment = new MyFragment();
    fragment.setArguments(bundle);

    return fragment;
}

…and easy to find and use by other developers.

Fragment fragment = MyFragment.newInstance(“Another nice label text”);

There are three common ways to add the fragment to the activity. The first and most common one is to check if the activity is started for the first time and just add it:

if (savedInstanceState == null) {
    getFragmentManager().beginTransaction()
            .replace(R.id.container, MyFragment.newInstance("label"))
            .commit();
}

The downside of doing this is that we have no reference to the fragment if we need to call on any of it’s functions. Another way of adding a fragment is to instead of checking for the first start of the activity we check if the fragment is present. We can find fragment by checking the container like this:

MyFragment fragment = (MyFragment) getFragmentManager().findFragmentById(R.id.container);
if (fragment == null) {
    fragment = MyFragment.newInstance("label");
    getFragmentManager().beginTransaction()
            .replace(R.id.container, fragment)
            .commit();
}

We can also check if a fragment is present by checking for the tag associated with it, we then need to give it a tag when we add it to the activity like this:

MyFragment fragment = (MyFragment) getFragmentManager().findFragmentByTag(TAG);
if (fragment == null) {
    fragment = MyFragment.newInstance("label");
    getFragmentManager().beginTransaction()
            .replace(R.id.container, fragment, TAG)
            .commit();
}

The two later examples gives us a reference to the fragment.

Parameters

Fragments are very similar to activities and the way you parse the bundle is the same with one minor exception, you need to call getArguments() instead of getIntent().getExtras().

protected void onCreate(Bundle savedInstanceState) {
    Bundle bundle = savedInstanceState == null ? getArguments() : savedInstanceState
    mLabel = bundle.getString(EXTRA_LABEL, "not found");
    mCounter = bundle.getInt(EXTRA_COUNTER, 0);
}

Just as with the activity if the ‘savedInstanceState’ parameter is null it means that the fragment is started for the first time.

protected void onSaveInstanceState(Bundle outState) {
    // Put all the values you got from starting the fragment.
    outState.putAll(getArguments()); 

    // If some of them are changed you still need to save them manually.
    outState.putInt(EXTRA_COUNTER, mCounter);

    super.onSaveInstanceState(outState);
}

Callbacks

Here we better be safe then sorry, callbacks within the lifecycle can be tricky so I try to make it safe by adding two layers. I usually like to have data methods on my activities and fragments like setBook(Book book) making it fairly easy to set the data.

To communicate from a fragment up to it’s parent activity or parent fragment you could use a callback interface. Fragment in fragment, No problem! Just use getChildFragmentManager.

public void onAttach(Activity activity) {
   super.onAttach(activity);
   
   // Check if parent fragment implements the interface otherwise check the activity.
   if (getParentFragment() instanceof CallbackListener) {
       mCallbackListener = (CallbackListener) getParentFragment();
   } else if (activity instanceof CallbackListener) {
       mCallbackListener = (CallbackListener) activity;
   }
}

Also remember to remove the listener if detached so you don’t make any calls to a detached fragment or activity.

public void onDetach() {
   super.onDetach();
   mCallbackListener = null;
}

If you have an empty implementation of the interface you can switch to that one in onDetach to get rid of null pointer checks.

public void onDetach() {
   super.onDetach();
   mCallbackListener = mEmptyCallback;
}

When you create a custom view you don’t use a ScrollView as a base but rather put your component inside a ScrollView when it is needed. You should treat your fragment the same way, don’t use a ScrollView as the base layout it is better to put a ScrollView around the fragment container where it is needed. I have in several projects been forced to put two fragments under each other and if both of them had a ScrollView as a base it will act really weird and be hard for the end user so keep your ScrollView away from the fragment.

Keeping it clean and simple!

I like to keep my Activity/Fragment as clean as possible, no other logic then inflating views and wiring the MVC and the lifecycle of course.

public class ItemDetailActivity extends Activity {

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Create the model (adaptor)
        ListAdapter adapter = new ListAdapter(getContext());
        
        // Create the view (inflate)
        setContentView(R.layout.activity_my);
        ListView listView = (ListView)findViewById(R.id.mylist);

        // Create the Controller
        ListController listController = new ListController(getContext(), listView, adapter);
    }
    
    ...

}

If I have more complex setup where I need some logic I try to keep it in a controller.

public class ItemDetailActivity extends Activity {

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Create the model (adaptor)
        mData = mService.getDataModels();
        
        // Create the view (inflate)
        setContentView(R.layout.activity_my);

        Button button = (ListView)findViewById(R.id.filter_button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mController.filter();
            }
        });

        // Create the Controller
        mController = new Controller(data);
    }
    
    ...

}

In some cases I moved the view inflation and MVC wiring to another class if it is to complexed just to make it easier to convert to a fragment later on if needed and keep the class easy to overview.

Complex logic

If I need more complex logic I prefer to move it to it’s own class. For example in a project a while ago we needed to have an activity that reacted as soon the phone was moved, to do that we started by using one sensor and put the code into the activity it was a bit messy but still it was okey. Later when we had to add another sensor as well and a simple version of the code looked like this:

public class MotionDetectionActivity extends Activity implements SensorEventListener {

    private SensorManager mSensorManager;
    private Sensor mAccelerometer;
    private Sensor mMagnetometer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        init();
    }

    private void init() {
        mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
        mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        mMagnetometer  = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
    }

    @Override
    protected void onResume() {
        mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME);
        mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME);

        super.onResume();
    }

    @Override
    protected void onPause() {
        mSensorManager.unregisterListener(this, mAccelerometer);
        mSensorManager.unregisterListener(this, mMagnetometer);

        super.onPause();
    }

    private void deviceIsNotMoving() {
        // Do what ever.
    }

    private void deviceIsMoving() {
        // Do what ever.
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (isMoving(event)) {
            deviceIsMoving();
        } else {
            deviceIsNotMoving();
        }
    }

    private boolean isMoving(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
            // if moving return true;
        }

        if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
            // if moving return true;
        }

        return false;
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {

    }
}

Even if this looks kinda clean imagine if you had a lot of other logic in here as well and with other stuff in there it becomes harder to see what code belongs to what. So let’s just say it will become messy quite quick. By writing our own motion sensor like this:

public class MotionSensor implements SensorEventListener {

    private final OnMotionSensorListener mListener;
    private       SensorManager          mSensorManager;
    private       Sensor                 mAccelerometer;
    private       Sensor                 mMagnetometer;

    public MotionSensor(Context context, OnMotionSensorListener listener) {
        mListener = listener;

        mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
        mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
    }

    public void register() {
        mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME);
        mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME);
    }

    public void unregister() {
        mSensorManager.unregisterListener(this, mAccelerometer);
        mSensorManager.unregisterListener(this, mMagnetometer);
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (isMoving(event)) {
            mListener.deviceIsMoving();
        } else {
            mListener.deviceIsNotMoving();
        }
    }

    private boolean isMoving(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
            // if moving return true;
        }

        if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
            // if moving return true;
        }

        return false;
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {

    }

    public interface OnMotionSensorListener {
        void deviceIsMoving();

        void deviceIsNotMoving();
    }
}

We gain two thing the first one is a cleaner activity.

public class MotionDetectionActivity extends Activity implements MotionSensor.OnMotionSensorListener {

    private MotionSensor mMotionSensor;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mMotionSensor = new MotionSensor(this, this);
    }

    @Override
    protected void onPause() {
        mMotionSensor.unregister();
        super.onPause();
    }

    @Override
    protected void onResume() {
        mMotionSensor.register();
        super.onResume();
    }

    @Override
    public void deviceIsNotMoving() {
        // Do what ever.
    }

    @Override
    public void deviceIsMoving() {
        // Do what ever.
    }
}

And the second one is a reusable component for the next time we need it. :-)

RecyclerView

Continue keeping it simple with the RecyclerView. I really only have two tips on keeping this one simple and clean and both are in the ViewHolder. By adding a newInstance method just like we did on the fragment we can make it really simple.

First one:

public class FirstViewHolder extends RecyclerView.ViewHolder {

    ...

    public static FirstViewHolder newInstance(ViewGroup viewGroup) {
        return new FirstViewHolder(
                       LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item, viewGroup, false)
                   );
    }
}

This means that in the adaptor we only need to call on this method, really simple…

public class FirstAdapter extends RecyclerView.Adapter {
    
    ...

    @Override
    public FirstViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
        return FirstViewHolder.newInstance(viewGroup);
    }
}

… or if you have different types just switch on the type:

@Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
        switch (type){
            case HEADER:
                return HeaderHolder.newInstance(viewGroup);
            case DIVIDER:
                return DividerHolder.newInstance(viewGroup);
            default:
                return FirstViewHolder.newInstance(viewGroup);
        }
    }

Second one:

By adding a bind method you can put all the model to view data inside the view holder…

public class FirstViewHolder extends RecyclerView.ViewHolder {

    ...

    public void bind(String str){
        mLabel.setText(str);
    }
   
    ...   
}

… keeping the adapter’s onBindViewHolder method nice and clean.

@Override
    public void onBindViewHolder(ViewHolder viewHolder, int position) {
        int type = getItemViewType(position)
        switch (type){
            case HEADER:
                ((HeaderHolder)viewHolder).bind(mData[position]);
            case DIVIDER:
                ((DividerHolder)viewHolder).bind(mData[position]);
            default:
                ((FirstViewHolder)viewHolder).bind(mData[position]);
        }
    }

If you want to read more about RecyclerView read my other blog post: Android RecyclerView – Simple List

This Post Has 5 Comments

  1. Miguel

    Hi, I like the instantiation tip.

    How do you recommed do it for the startActivityForResult method?

    Thanks in advance. And thanks for the post.

    1. Per-Erik Bergman

      I would do the same but change the context param to an activity, like this:

      public static void startActivityForResult(Activity activity, int sectionId, int lessonId) {
      Intent launchIntent = getIntent(context, sectionId, lessonId);
      activity.startActivityForResult(launchIntent, ...);
      }

  2. Miguel

    Thanks for the reply.

    And another one question… How do you pass data easily from an Activity 1 to an Activity 4 without coding so much code in Activities 1, 2, 3, 4 and their fragments? And how is the best way to go back the result from Activity 4 to Activity 1 closing the intermediates Activities and Fragments?

    Thanks in advance again. ^^

  3. Ellaps

    Miguel, for pass data between activities/fragments – you can use libraries like Otto, EventBus.
    Correct me if I wrong)

    1. Per-Erik Bergman

      You could but I would not recommend it. They are hard to debug and in theory all activities can be shut down by the system at any given time so it is better and safer to store things in a data model if startActivityForResult is not enough.

Leave a Reply