AndroidModularSample


Source link: https://github.com/RoRoche/AndroidModularSample

AndroidModularSample

FSM on Android: how to reach app modularity?

Introduction

In this document, I want to explain how I ended up with a FSM as a solution to make Android application development fully-modular. That's why this document is organized in steps, describing the journey I made, from discovering the EasyFlow FSM to using it for modularity purposes.

When talking about modularity, all developers agree on a standard definition:

  • divide a program into separated sub-programs (called modules) according to the features to implement,
  • group similar functions in the same unit of code.

Advantages:

  • ease incremental builds and deliveries,
  • module is unit-testable,
  • modules can be added, modified or removed without any impact on one another,
  • modules can be reused.

But on some implementations, I didn't agree with some choices. For example, to navigate to the next screen, a module knows the name of this next screen thanks to a constant (from the hosting application or from the next module itself): this introduces a high-coupling that stinks. To my mind, to be fully independant and agnostic, a module must not depend on a constant from anywhere else. At best, it exposes constants to be used by the hosting application. So, how to transit from one screen to the next one? The answer became clear: thanks to events. Indeed, events bring the needed abstraction: the module fires an event, the hosting application receives this event and acts accordingly. This is how I discovered the "event-driven development" paradigm.

According to this paradigm, the flow of the program is determined by events (user actions, network requests, sensors, timer, other threads, etc.). A specific piece of code (often called the "event listener" or "main loop" according to the language) detects that an event has occurred and performs the corresponding event "handler" (a callback method for example). The resulting computation changes the program state.

Finally, keeping in mind my final goal, I remembered the terms used by the Android SDK: when an Activity is destroyed, it mentions the fact of saving and restoring the "instance state". And all became clear for me: a "screen" can be considered as a "state" of the application, and navigation is somehow a finite state machine.

A finite state machine (FSM) is defined by:

  • sequential logic circuits,
  • finite number of states,
  • one state at a time (the current state),
  • change from one state to another by triggering an event (a transition).

For Java, the implementation I've chosen is EasyFlow, because:

  • it's very simple to set up,
  • it's possible to define a global context (useful to pass arguments for example),
  • states are easily defined by declaring an enum implementing StateEnum,
  • events are defined easily by declaring an enum implementing EventEnum,
  • it provides a fluent API, which allows developers to declare the state machine in a clear and intelligible way (especially when combined with lambda expressions),
  • it's possible to declare callbacks to perform specific jobs when entering or leaving a state.

FSM to manage view states

Let's consider a typical example of a simple screen loading data from the network to display the result. Below we can see a state diagram corresponding to this screen.

graph LR
  Idle -->|on first load| Loading
  Loading -->|on error| Error
  Loading -->|on success| Content
  Content -->|on refresh| Loading
  Error -->|on retry| Loading 

The associated XML layout is not very relevant at this point. But just consider the following declarations in the Activity are valid:

@BindView(R.id.ActivityMain_TextView_Content) TextView mTextViewContent; @BindView(R.id.ActivityMain_ProgressBar) ProgressBar mProgressBar; @BindView(R.id.ActivityMain_ViewGroup_Content) LinearLayout mViewGroupContent; @BindView(R.id.ActivityMain_ViewGroup_Error) LinearLayout mViewGroupError;

Now, using the great library called Switcher, we can define some fields as follows:

@Override protected void onCreate(Bundle savedInstanceState) {

  super.onCreate(savedInstanceState);

  setContentView(R.layout.activity_main);

mUnbinder = ButterKnife.bind(this);

mSwitcher = new Switcher.Builder(this)

 .addContentView(mViewGroupContent)

 .addErrorView(mViewGroupError)

 .addProgressView(mProgressBar)

 .build();

Now, to setup the FSM corresponding to the state diagram, we need to define the available States, Events and Context as follows:

enum States implements StateEnum {

  LOADING, ERROR, CONTENT 
}
  enum Events implements EventEnum {

  onError, onSuccess, onRetry, onRefresh 
}
  class FlowContext extends StatefulContext {
 
}

And then we can set up our FSM programmatically as exposed below:

EasyFlow<FlowContext> flow =

from(States.LOADING).transit(

on(Events.onError).to(States.ERROR).transit(

 on(Events.onRetry).to(States.LOADING)

  ),

  on(Events.onSuccess).to(States.CONTENT).transit(

 on(Events.onRefresh).to(States.LOADING)

  )

);

And now we have to define what to perform when entering a given state (with the help of the retrolambda library):

flow.whenEnter(States.LOADING, (final StatefulContext context) -> startRequest());
 flow.whenEnter(States.CONTENT, (final StatefulContext context) -> mSwitcher.showContentView());
 flow.whenEnter(States.ERROR, (final StatefulContext context) -> mSwitcher.showErrorView());

We can start the FSM as follows:

flow.executor(new UiThreadExecutor());
 mFlowContext = new FlowContext();
 flow.start(mFlowContext);

The Executor instance allows developpers to configure the type of thread used to perfom FSM operations (such as whenEnter). It looks like:

public class UiThreadExecutor implements Executor {

  private Handler mHandler = new Handler(Looper.getMainLooper());

@Override
  public void execute(Runnable command) {

mHandler.post(command);

  
}
 
}

To perform the network request in this example, I use the Volley library as follows:

private void startRequest() {

  mSwitcher.showProgressViewImmediately();

  StringRequest request = new StringRequest(

 "https://api.github.com/users/RoRoche",

 (final String response) -> {

  mTextViewContent.setText(response);

  try {

mFlowContext.trigger(Events.onSuccess);

  
}
 catch (LogicViolationError logicViolationError) {

Log.e(TAG, "startRequest", logicViolationError);

  
}

 
}
,

 (final VolleyError error) -> {

  try {

mFlowContext.trigger(Events.onError);

  
}
 catch (LogicViolationError logicViolationError) {

Log.e(TAG, "startRequest", logicViolationError);

  
}

 
}
);

  mQueue.add(request);
 
}

All that remains for me to do, therefore, is to implement methods listening to user interactions:

@OnClick(R.id.ActivityMain_Button_Refresh) public void onClickRefresh() {

  try {

mFlowContext.trigger(Events.onRefresh);

  
}
 catch (LogicViolationError logicViolationError) {

Log.e(TAG, "onClickRefresh", logicViolationError);

  
}
 
}
  @OnClick(R.id.ActivityMain_Button_Retry) public void onClickRetry() {

  try {

mFlowContext.trigger(Events.onRetry);

  
}
 catch (LogicViolationError logicViolationError) {

Log.e(TAG, "onClickRetry", logicViolationError);

  
}
 
}

Putting it all together and it works like a charm! Transitions are logical, fluid and well-defined. The logical code is devoted to the FSM. The rest of the code consists in triggering the suitable event.

FSM to manage app navigation

Following the same logic, I decided to set up 2 screens and manage navigation thanks to a FSM.

Why Conductor?

First of all, it's important to focus on the Conductor library I chose to create a "View-based application".

Here are many advantages of using this library:

  • Navigation concerns
  • Simple to build an instance of Controller and provide its dependencies (no args Bundle like when using Fragment)
  • Bring forward the ViewController concept
  • Pretty transitions
  • Easy to integrate in a MVP or VIPER architecture

"Screens" as "States"

The idea came to me that, in fact, a screen displayed to the user can be considered as a state of the application. I mean: for example, when presenting a screen to log in, it's a state "waiting for login", isn't it? And the event to change the app state is "valid login provided", isn't it?

The idea is to use Conductor to define each screen, and use the Activity to monitor the interactions and navigation.

To keep each screen independant, I decided to use the StatefulContext implementation to hold arguments as a Bundle instance (pretty familiar in the Android world, isn't?). This way, the implementation looks like:

public class FlowContext extends StatefulContext {

  private final Bundle mArgs = new Bundle();

public Bundle args() {

return mArgs;
  
}
 
}

Now it's time to design the state diagram of the features to implement:

graph LR
  WaitingForLogin -->|on valid login provided| ShowingWelcome
  ShowingWelcome -->|on back pressed| WaitingForLogin 

Very simple in fact. So it's time to define our first screen ("waiting for login"):

public class FirstController extends Controller {

  //region Constants
  private static final String ARG_KEY_LOGIN = "LOGIN";
  //endregion

//region Fields
  private FlowContext mFlowContext;
  private TextInputEditText mEditTextLogin;
  //endregion

//region Constructors
  public FirstController() {

this(new FlowContext());

  
}

public FirstController(FlowContext flowContext) {

super();

mFlowContext = flowContext;
  
}

  //endregion

//region Controller
  @NonNull
  @Override
  protected View onCreateView(LayoutInflater inflater, ViewGroup container) {

View view = inflater.inflate(R.layout.first_controller, container, false);

mEditTextLogin = (TextInputEditText) view.findViewById(R.id.FirstController_EditText_Login);

view.findViewById(R.id.FirstController_Button_Start).setOnClickListener(new View.OnClickListener() {

 @Override

 public void onClick(View pView) {

  onClickButtonStart();

 
}

}
);

return view;
  
}

  //endregion

//region User interaction
  private void onClickButtonStart() {

String login = mEditTextLogin.getText().toString();

if (TextUtils.isEmpty(login)) {

 mEditTextLogin.setError(getApplicationContext().getString(R.string.login_error));

}
 else {

 try {

  mFlowContext.args().putString(ARG_KEY_LOGIN, login);

  mFlowContext.trigger(Events.loginProvided);

 
}
 catch (final LogicViolationError poLogicViolationError) {

 
}

}

  
}

  //endregion

//region FSM
  public enum States implements StateEnum {

WAITING_LOGIN
  
}

public enum Events implements EventEnum {

loginProvided
  
}

public static String getLogin(FlowContext flowContext) {

return flowContext.args().getString(ARG_KEY_LOGIN);

  
}

  //endregion 
}

This screen defines its available states and events. It's necessary to pass the FlowContext to its constructor. After some logical controls, this screen puts the provided value in the FlowContext arguments and triggers the loginProvided event. It's its single responsability: display an interface to fill a login value, control it and notify of the sequence success.

In the same way, let's define the second screen (for simplicity, it just displays the provided login):

public class SecondController extends Controller {

  //region Args
  private final String mLogin;
  //endregion

//region Constructors
  public SecondController() {

this("");

  
}

public SecondController(String login) {

super();

mLogin = login;
  
}

  //endregion

//region Controller
  @NonNull
  @Override
  protected View onCreateView(LayoutInflater inflater, ViewGroup container) {

View view = inflater.inflate(R.layout.second_controller, container, false);

TextView textViewWelcome = (TextView) view.findViewById(R.id.SecondController_TextView_Welcome);

textViewWelcome.setText(getApplicationContext().getString(R.string.welcome, mLogin));

return view;
  
}

  //endregion

//region FSM
  public enum States implements StateEnum {

SHOWING_WELCOME
  
}

  //endregion 
}

Much simpler than the previous one! But now you should ask yourself "wait! where are these screens built? where is the navigation logic? where is the back event?". Here is my point: in the Activity. No screen should know how to transit from one state to another. They just have to define the state they represent and the events that can be triggered. Moreover, for me, this second screen must not know the "back pressed" event. It is not its responsability to navigate back. So, now, it's time to have a look at the Activity:

Navigation between screens

public class MainActivity extends AppCompatActivity {

//region Fields
  private Router mRouter;
  private EasyFlow<FlowContext> mFlow;
  private FlowContext mFlowContext;
  //endregion

//region Lifecycle
  @Override
  protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

 ViewGroup container = (ViewGroup) findViewById(R.id.ViewGroup_Container);

mRouter = Conductor.attachRouter(this, container, savedInstanceState);

 mFlow =

  from(FirstController.States.WAITING_LOGIN).transit(

 on(FirstController.Events.loginProvided).to(SecondController.States.SHOWING_WELCOME).transit(

on(Events.backPressed).to(FirstController.States.WAITING_LOGIN)

 )

  );

 mFlow.executor(new UiThreadExecutor());

 mFlow.whenEnter(FirstController.States.WAITING_LOGIN, (FlowContext context) -> {

 if (!mRouter.hasRootController()) {

  mRouter.setRoot(RouterTransaction.with(new FirstController(context)));

 
}

}
);

 mFlow.whenEnter(SecondController.States.SHOWING_WELCOME, (FlowContext context) -> {

 SecondController loController = new SecondController(FirstController.getLogin(context));

  AndroidModularApplication.getInstance().getComponentSecondController().inject(loController);

  RouterTransaction transaction = RouterTransaction.with(loController)

.pushChangeHandler(new FadeChangeHandler())

.popChangeHandler(new FadeChangeHandler());

  mRouter.pushController(transaction);

}
);

 mFlow.whenLeave(SecondController.States.SHOWING_WELCOME, (FlowContext context) -> {

 context.args().clear();

}
);

 mFlowContext = new FlowContext();

mFlow.start(mFlowContext);

  
}

@Override
  public void onBackPressed() {

try {

 mFlowContext.trigger(Events.backPressed);

}
 catch (LogicViolationError logicViolationError) {

}

 if (!mRouter.handleBack()) {

 super.onBackPressed();

}

  
}

  //endregion

//region FSM
  public enum Events implements EventEnum {

backPressed
  
}

  //endregion 
}

All the valuable jobs are done in the "whenEnter" sequences. Indeed, when entering the first state, we create and push the first screen to the Conductor-specific Router instance. When entering the second state, we build the second screen, pass the arguments to it and push it to the Router using a fading RouterTransaction.

We override the onBackPressed and trigger the corresponding event to update the FSM current state. Then if the Router instance allows "back" at this level, this event is processed.

Finally, it is not that much complicated to do. But we gain a clean design, with a perfect application of the "Single responsability principle". This Android application is now a valuable combination of "states" and "events".

FSM to make app modular

Screns as independent modules

You know what? It's the simplest step of this article: you just have to create two Android modules ("first" and "second") thanks to the Android Studio wizard, drag and drop each piece of code in the corresponding module and that's it! Well, not really: you have to create a "common" module to place the FlowContext and UiThreadExecutor. Both "first" and "second" modules reference this "common" module. Now, these modules can be successfully referenced by the application.

Too simple maybe? So let's go a step further with the dependency injection topic.

DI ready: example with Dagger 2

Now you have an application divided into multiple screens (states) and responding to various events, you may ask yourself how to define and provide the dependencies needed by your screens. To introduce a solution, I will use the Dagger 2 library.

Suppose we want to display the current date on the second screen of our application. I'll start by defining an interface in the "second" module:

public interface IDateFormatter {

  String format(Date date);
 
}

In the "app" module, I'm going to implement this one:

public final class DateFormatter implements IDateFormatter {

//region Constants
  private static final String DATE_FORMAT = "dd/MM/yyyy";
  private static final SimpleDateFormat sSimpleDateFormat = new SimpleDateFormat(DATE_FORMAT);

  //endregion

//region IDateFormatter
  @Override
  public String format(Date date) {

return sSimpleDateFormat.format(date);

  
}

  //endregion 
}

Now, let's create the suitable Dagger 2 Module as follows:

@Module public class ModuleSecondController {

@Provides
  @Singleton
  public IDateFormatter providesDateFormatter() {

return new DateFormatter();

  
}
  
}

And the Component to configure the second screen:

@Singleton @Component(modules = {
ModuleSecondController.class
}
) public interface ComponentSecondController {

void inject(SecondController secondController);
  
}

The next step is to subclass the Android Application class and build the Module and Component as follows:

public class AndroidModularApplication extends Application {

//region Static field
  private static AndroidModularApplication sInstance;
  //endregion

//region Field
  private ComponentSecondController mComponentSecondController;
  //endregion

//region Overridden method
  @Override
  public void onCreate() {

super.onCreate();

sInstance = this;

mComponentSecondController = DaggerComponentSecondController.builder()

  .moduleSecondController(new ModuleSecondController())

  .build();

  
}

  //endregion

//region Static getter
  public static AndroidModularApplication getInstance() {

return sInstance;
  
}

  //endregion

//region Getter
  public ComponentSecondController getComponentSecondController() {

return mComponentSecondController;
  
}

  //endregion 
}

And now, the final step takes place in the MainActivity. It's time to inject dependencies in the second screen. So we're back to the "whenEnter" sequence when building the second screen:

SecondController controller = new SecondController(FirstController.getLogin(context));
  AndroidModularApplication.getInstance().getComponentSecondController().inject(controller);
  RouterTransaction transaction = RouterTransaction.with(controller)

.pushChangeHandler(new FadeChangeHandler())

.popChangeHandler(new FadeChangeHandler());
  mRouter.pushController(transaction);

Eventually, we can add a preconditions to the second screen (defensive programming!) as follows:

protected View onCreateView(LayoutInflater inflater, ViewGroup container) {

  Preconditions.checkNotNull(mDateFormatter, "Field mDateFormatter is null, did you miss to inject it with your dependency injection mechanism?");

This way, you can inject any element you want (data store, user preferences, etc.). But the targetted screen remains agnostic, independent and highly configurable.

Conclusion

  • An easy way to deal with the states of a screen

  • A new approach to set up the navigation flow of an application

  • A solution to build trully reusable modules

  • High level of configuration with DI

  • Perspectives: what about a library of existing modules, used by a "drag & drop" Web interface to design an entire application in a user-friendly way?

Bibliography

Logo credits

Food graphic by Freepik from Flaticon is licensed under CC BY 3.0. Made with Logo Maker

Resources

A simple ViewPager extends that provide scroll based animation like Jazz Hands for iOS.

This is a small library to create floating windows with balloons.

A very beautiful and easy-to-use app that allows to copy code to the user's clipboard, when user receives message verification code.

KAM

KAM is an apk manager, backup, restore and more.

A comprehensive list of string-arrays that you may need on a daily basis when developing an Android app.

A dead simple way to to add tooltips for your Android app.

Topics


2D Engines   3D Engines   9-Patch   Action Bars   Activities   ADB   Advertisements   Analytics   Animations   ANR   AOP   API   APK   APT   Architecture   Audio   Autocomplete   Background Processing   Backward Compatibility   Badges   Bar Codes   Benchmarking   Bitmaps   Bluetooth   Blur Effects   Bread Crumbs   BRMS   Browser Extensions   Build Systems   Bundles   Buttons   Caching   Camera   Canvas   Cards   Carousels   Changelog   Checkboxes   Cloud Storages   Color Analysis   Color Pickers   Colors   Comet/Push   Compass Sensors   Conferences   Content Providers   Continuous Integration   Crash Reports   Credit Cards   Credits   CSV   Curl/Flip   Data Binding   Data Generators   Data Structures   Database   Database Browsers   Date &   Debugging   Decompilers   Deep Links   Dependency Injections   Design   Design Patterns   Dex   Dialogs   Distributed Computing   Distribution Platforms   Download Managers   Drawables   Emoji   Emulators   EPUB   Equalizers &   Event Buses   Exception Handling   Face Recognition   Feedback &   File System   File/Directory   Fingerprint   Floating Action   Fonts   Forms   Fragments   FRP   FSM   Functional Programming   Gamepads   Games   Geocaching   Gestures   GIF   Glow Pad   Gradle Plugins   Graphics   Grid Views   Highlighting   HTML   HTTP Mocking   Icons   IDE   IDE Plugins   Image Croppers   Image Loaders   Image Pickers   Image Processing   Image Views   Instrumentation   Intents   Job Schedulers   JSON   Keyboard   Kotlin   Layouts   Library Demos   List View   List Views   Localization   Location   Lock Patterns   Logcat   Logging   Mails   Maps   Markdown   Mathematics   Maven Plugins   MBaaS   Media   Menus   Messaging   MIME   Mobile Web   Native Image   Navigation   NDK   Networking   NFC   NoSQL   Number Pickers   OAuth   Object Mocking   OCR Engines   OpenGL   ORM   Other Pickers   Parallax List   Parcelables   Particle Systems   Password Inputs   PDF   Permissions   Physics Engines   Platforms   Plugin Frameworks   Preferences   Progress Indicators   ProGuard   Properties   Protocol Buffer   Pull To   Purchases   Push/Pull   QR Codes   Quick Return   Radio Buttons   Range Bars   Ratings   Recycler Views   Resources   REST   Ripple Effects   RSS   Screenshots   Scripting   Scroll Views   SDK   Search Inputs   Security   Sensors   Services   Showcase Views   Signatures   Sliding Panels   Snackbars   SOAP   Social Networks   Spannable   Spinners   Splash Screens   SSH   Static Analysis   Status Bars   Styling   SVG   System   Tags   Task Managers   TDD &   Template Engines   Testing   Testing Tools   Text Formatting   Text Views   Text Watchers   Text-to   Toasts   Toolkits For   Tools   Tooltips   Trainings   TV   Twitter   Updaters   USB   User Stories   Utils   Validation   Video   View Adapters   View Pagers   Views   Watch Face   Wearable Data   Wearables   Weather   Web Tools   Web Views   WebRTC   WebSockets   Wheel Widgets   Wi-Fi   Widgets   Windows   Wizards   XML   XMPP   YAML   ZIP Codes