Android development: problems and solutions (Part 1)

Software development is basically about creating problems. As we are writing code, we are also making problems. Problems grow when we have more and more code. It’s about how do we structure our code, how do we make it reusable, easy to test, modify and extend.

Today I want to share the story of how do I and my Android team made the problems and solved (some of) them.

Some first days I came to the team are so difficult. We have a quite big app (http://goo.gl/jikbzu) with a quite a lot of problems:

1. We have god classes (activities, fragments with thousands lines of code).

They are responsible for doing almost things (fetching data from networks, processing business logics,…). This will leads to many problems: difficult to maintain because we need to scroll and scroll through the code to see where we do something, unable to unit tests because the logics were mixed in activities, fragments, unable to reuse the logic when we need that somewhere else,…

The solution here is quite simple: Let activities, fragment do what they need to do and delegates other job to other components.

For example:


public class DiscoveryFragment extends Fragment {
private UnifiedLocationProvider mProvider;
private CityManager mCityManager;

private double lat;
private double lng;


@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

Location location = mProvider.getLastKnownLocation();

// BEGIN OF LOGIC
double latitude = 0;
double longitude = 0;
if (location != null) {
latitude = location.getLatitude();
longitude = location.getLongitude();
}
else {
City city = mCityManager.getCity();
Coordinates coordinates = null;
if (city != null) {
coordinates = LocationUtil.fromCoordinatesString(city.getCoordinates());
}
if (coordinates != null) {
latitude = coordinates.getLat();
longitude = coordinates.getLng();
}
}
// END OF LOGIC

lat = latitude;
lat = longitude;
        // Do something with the lat/lng
}
}

Then we extract the logic into one class and name it LatLngProvider. Looks like this:

public class LatLngProvider {
private LocationProvider mProvider;
private CityManager mCityManager;

public LatLngProvider(LocationProvider provider, CityManager cityManager) {
mProvider = provider;
mCityManager = cityManager;
}

public LatLng getLatLng() {
double latitude = 0;
double longitude = 0;
Location location = mProvider.getLastKnownLocation();
if (location != null) {
latitude = location.getLatitude();
longitude = location.getLongitude();
}
else {
City city = mCityManager.getCity();
Coordinates coordinates = null;
if (city != null) {
coordinates = LocationUtil.fromCoordinatesString(city.getCoordinates());
}
if (coordinates != null) {
latitude = coordinates.getLat();
longitude = coordinates.getLng();
}
}
return new LatLng(latitude, longitude);
}
}
public class DiscoveryFragment extends Fragment {
private LatLngProvider mProvider;

@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

double lat = mProvider.getLat();
double lng = mProvider.getLng();
// Do something with the lat/lng
}
}

From now we can unit tests the LatLngProvider easily by mocking it’s dependencies, we also can reuse LatLngProvider every where instead of copy/paste that piece of logic. More than that, when ever we want to modify something related to lat/lng we just need to navigate to LatLngProvider class, no need to find it somewhere in an activity/fragment.

2. Instantiate objects

OK, the first problem was solved, but it created one more problem: how do we instantiate LatLngProvider object? Luckily we already have solution for this in Android: Dagger 2. In short, it helps us to write the instantiate code.

See more: http://antonioleiva.com/dependency-injection-android-dagger-part-1/

3. We have nested callbacks, our callbacks are messy

We were using callbacks, it’s fine, but in networking tasks, it caused some problems. Callbacks are come alone, so we need to do all the things manual. Let see how it looks, this callback get calls when user request got result:

@Override
public void onSuccess(UserResponse response) {
// Show user details
}

But what we want is not only like that, we want a lot more. Before we can do the request, we need to get parameters somewhere which is also async then after we have user, we need to store it in the disk, we need to notify to other activities/fragments to refresh. So our callbacks will become like this:

@Override
public void onReceivedUserParams(Params params) {
// Executes user request
}
@Override
public void onSuccess(UserResponse response) {
User user = response.user();

// Show user details

// Save to disk
mUserManager.setUser(userResponse.user());

// Notify to listeners
mRxBus.post(user);
}

So, it starting to messy now. Inside the callback, we make another request. We have UserResponse, not User, so we need to get User from UserResponse. Then we need to save and notify, everywhere we requests for a user, we need to do it as well.

Our solutions in this case is RxJava. With RxJava, it will look like:

mUserParams.getPrams()
.flatMap(new Func1<Params, Observable<UserResponse>>() {
@Override
public Observable<UserResponse> call(Params params) {
return mUserAPI.updateUser(params);
}
})
.map(new Func1<UserResponse, User>() {
@Override
public User call(UserResponse userResponse) {
return userResponse.user();
}
})
.doOnNext(new Action1<User>() {
@Override
public void call(final User user) {
mUserManager.setUser(User);
mRxBus.post(User);
}
});

It much cleaner and more testability. We also should create a class to wrap the code above, then we can use it everywhere and no need to care about what it does inside.

4. Lot of code for update the UIs

We were used setText, setVisibility, setXYZ… for everything. Very easy to figure out that we need to write a lot of code to declare the fields and set it’s values.

Luckily that Google also knows that, they have Data Binding. So just use it https://developer.android.com/topic/libraries/data-binding/index.html

Also, as we discuss, it’s better if we use data binding with MVVM. Therefore, the final solution is use Data Binding + MMVM.

When solved this problem, we also made mistakes. Actually, there are two things to bind, one is data and the other one is action (mentioned in the guide above) and it should be handles by two components . But we are not really care about action binding, we just use ViewModel for data and action. For simple case, it’s ok.

public class UserItemViewModel {
private Context mContext;
private User mUser;

public UserItemViewModel(Context context, User user) {
mContext = context;
mUser = user;
}

// Data
public String greeting() {
return mContext.getString(R.string.home_greeting, mUser.getName());
}

// Action
public void onViewUserDetailsClicked(View view) {
UserDetailsActivity.start(mContext, mUser);
}
}

As the app grow, we have bigger and bigger ViewModel and it mixed of data and action handler. This ViewModel creates by other ViewModel and it got many subclasses. The worst thing happen when we need to add analytics. As we are changing the constructor of UserItemViewModel, we need to modify the code in UserItemViewModel, in the holder class that creates UserItemViewModel, in where we creates the holder class and in every subclasses as well.

public class UserItemViewModel {
private Context mContext;
private User mUser;
private Tracker mTracker;

public UserItemViewModel(final Context context, final User user, final Tracker tracker) {
mContext = context;
mUser = user;
mTracker = tracker;
}

// Action
public void onViewUserDetailsClicked(View view) {
UserDetailsActivity.start(mContext, mUser);

mTracker.viewUserDetailsClickEvent().send();
}
}

The problem seems complex but after look back we realize that it quite simple: We violated the “Single responsibility” principle in SOLID. Our UserItemViewModel has two responsibilities. We immediately spawn one more class, UserDetailsHandler which is responsible for user clicks event. The code is much cleaner and easier to modify.

These are just some first problem we realized an solved, we have more. Will tell you in the next section!