Meta Programming in Android
https://images.app.goo.gl/qt7EJhA2BkBUujjcA

Meta Programming in Android

Year after year we are getting rid of the boilerplate code that we need to write for small and simple tasks in Android, and in this article, we will go through old but powerful technique to cut down our efforts ... in fact, it is there since the very first Android version, but after introducing features like LifecycleObserver interface in Android, it became much more effective and can suite most of the use cases we need

No need for Base classes

We usually start any android project with putting the BaseActivity and BaseFragment classes to put there the common operations we need to use in all of our Activities and Fragments, those operations can be divided into 2 categories

  • operations that are executed based on life cycle events in multiple Activities/Fragments, like drawing some stuff in onCreate(), register and unregister stuff in onStart() and onPause(), etc...
  • operations that we need to invoke from any Activity/Fragment, like showDialog(), showToast(), etc...

For the operations of the second category, it is preferred to declare them as stand alone functions that we invoke when we need, no need to keep them in a Base class, they can be functions that take an Activity/Fragment in there parameter, and do what ever you need, and put them in there own package in the UI layer

For the first category, we face a challenge, is that we need the Base class to call them for us in the proper life cycle events, instead of we keep calling them in every Activity/Fragment, and here comes the solution in this article

ActivityLifecycleCallbacks and FragmentLifecycleCallbacks

Although there are some differences in how to register each one, but the end result is the same for both, when our Activity or Fragment life cycle methods call there super counterparts, they invoke the code inside those callbacks

let us start with Activities, this is the ActivityLifecycleCallbacks interface declaration :

public interface ActivityLifecycleCallbacks {
    void onActivityCreated(Activity activity, Bundle savedInstanceState);
    void onActivityStarted(Activity activity);
    void onActivityResumed(Activity activity);
    void onActivityPaused(Activity activity);
    void onActivityStopped(Activity activity);
    void onActivitySaveInstanceState(Activity activity, Bundle outState);
    void onActivityDestroyed(Activity activity);
}

So now, to hook up any operation in a life cycle method for an Activity, all we need to do is to implement the desired life cycle method in the ActivityLifecycleCallbacks, and register it through the Application class of our App

So for example, if we need to show a Toast with the Activity title in it's onCreate(), we will do the following

1- Implement the interface :

class ToastDisplayer : Application.ActivityLifecycleCallbacks {


    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle) 
    {
        Toast.makeText(activity, activity.title, Toast.LENGTH_SHORT).show()
    }
    
    ...
    // leave the rest of not needed methods empty

}

2- Register the implementer

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        ...
        registerActivityLifecycleCallbacks(ToastDisplayer())
    }

}

And this is how we execute code among all of our Activities, but in real life we use Base class because we want to pass parameters to the code that will be executed in the life cycle events, like for example, we do not want to show the title toast except for activities with title, so here we have multiple ways, and one of them is Annotations, which is the cleanest way for them as we will see

Working with Annotations

1- Declare the Annotation

It should be visible at Runtime to our JVM, yes we will use reflections, which you already use with ViewModelProviders and many other stuff and you will see the performance your self

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class ShowTitleToast

2- Scan for the Annotation

Now in our ActivityLifecycleCallbacks implementer, we will check if the Activity is annotated with this annotation (which means it wants to show title toast) or not

class ToastDisplayer : Application.ActivityLifecycleCallbacks {


    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle) 
    {
        if(activity.javaClass.isAnnotationPresent(ShowTitleToast::class.java)) {
            Toast.makeText(activity, activity.title, Toast.LENGTH_SHORT).show()
        }
    }
    ...    
}

3- Annotate the desired classes

Now that we have our ActivityLifecycleCallbacks implementer handling the annotation, we can annotate our Activity class that we want it to show the toast :

@ShowTitleToast
class MainActivity : AppCompatActivity()

Passing parameters through Annotations

Let's be more practical, in our example, we will still need to set the Activity title for all the annotated Activities, while we can make the annotation itself pass this parameter to our ActivityLifecycleCallbacks implementer

1- Declare Annotation with parameter

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class ShowTitleToast(val title: String)

2- Pass the parameter to the Annotations from Activities

@ShowTitleToast("Home")
class MainActivity : AppCompatActivity()


@ShowTitleToast("Settings")
class SettingsActivity : AppCompatActivity()

3- Handle parameter from ActivityLifecycleCallbacks implementer

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle){

    val annotation =  
        activity.javaClass.getAnnotation(ShowTitleToast::class.java) ?: return

    activity.title = annotation.title
    
    Toast.makeText(activity, activity.title, Toast.LENGTH_SHORT).show()

}


Now in our simple scenario, we made a custom action and passed to it the parameters through annotation, this can also apply to things like setting the content view, drawing the Toolbar or applying some flags to the window which makes the Activity a full screen or what ever

Real Life example

In the following example, the requirement is to make the user notice when the internet is disconnected from the device, so when this happens, an image should be grayed out (displayed in a gray color), once the internet is back, it should return back to the normal color, and all of this is achieved through Annotation

Because this is code from real project, it is in Java and contains RxJava code, with some custom methods as well, so for each class I will explain what it is doing in the process

1- Declare OnNetworkChangedListener

This class will be responsible to listen for the Network state, and report to it's caller weather we are connected or not through a Boolean value ... although it is a Class, technically it is a function but in terms of Java (a functional class), as it is a class with one public method, and to make sure it is dealt with as a function in Java code, it implement's one of the functional interfaces of Java, and this approach is very good if you want your classes to follow the Single Responsibility Principle, Open Closed Principle and Dependency Inversion Principle as well

public class OnNetworkChangedListener implements Function<AppCompatActivity, Observable<Boolean>>, Logger {




    @Override
    public Observable<Boolean> apply(AppCompatActivity context) {
        BehaviorSubject<Boolean> connection = BehaviorSubject.create();
        registerNetworkBroadcast(context, connection);
        return connection;
    }




    private void registerNetworkBroadcast(AppCompatActivity context, Subject<Boolean> connection) {


        ConnectivityManager manager = connectivityManager(context);


        if (manager == null) return;


        context.getLifecycle().addObserver(receiverObserver(context, receiver(connection, manager)));


    }


    private ConnectivityManager connectivityManager(AppCompatActivity context) {
        return Objects
                .maybe(context.getSystemService(Context.CONNECTIVITY_SERVICE))
                .cast(ConnectivityManager.class)
                .blockingGet();
    }


    @NotNull
    private DefaultLifecycleObserver receiverObserver(AppCompatActivity context, BroadcastReceiver receiver) {
        return new DefaultLifecycleObserver() {
            @Override
            public void onStart(@NonNull LifecycleOwner owner) {
                context.registerReceiver(receiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
            }


            @Override
            public void onStop(@NonNull LifecycleOwner owner) {
                context.unregisterReceiver(receiver);
            }
        };
    }


    @NotNull
    private BroadcastReceiver receiver(Subject<Boolean> connection, ConnectivityManager manager) {
        return new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                connection.onNext(isOnline(manager));
            }
        };
    }


    private Boolean isOnline(ConnectivityManager connectivityManager) {
        return Objects.maybe(connectivityManager.getActiveNetworkInfo())
                .map(NetworkInfo::isConnected)
                .doOnError(this::error)
                .onErrorReturnItem(false)
                .blockingGet(false);
    }
    
}

  • Objects.maybe() is a function that checks if it's parameter is not null, it will return as Maybe.just(), else it will return Maybe.empty() ... this function is there because RxJava does not accept null parameters even for it's Maybe type
  • error() is a default method in Logger interface, which safely logs errors to Crashlytics
  • This function takes an Activity in it's parameter, and it creates the BroadcastReceiver that listens to network status, and using the LifecycleObserver interface, it registers that broadcast receiver in Activity's onStart(), and unregister it in onStop()
  • The return of the Function is an Rx Subject, an Object that holds one item, in our case a Boolean, and it puts in that Subject "true" if we are connected, or "false" if we are disconnected ... this Subject will be observed by some one else and they will take action every time a new value is put in it

2- Declare the Annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface NetworkStateListenerImage {

    int value();

}

This is how we declare the annotation in Java, ElementType.TYPE in Java is similar to AnnotationTarget.CLASS in Kotlin, this annotation expects an Integer value to be passed to it when it is declared

3- Declare the Annotation Scanner - NetworkStateScanner

Since the logic to handle this Annotation is more complex than just showing a toast, it is put in it's own Function (Functional Class in Java), which is as follows :

public class NetworkStateScanner implements Consumer<Activity>, Logger, Resources {
    @Override
    public void accept(Activity activity) {


        Disposable disposable = bindImageToNetworkState(activity);


        ((AppCompatActivity) activity).getLifecycle().addObserver(new DefaultLifecycleObserver() {
            @Override
            public void onDestroy(@NonNull LifecycleOwner owner) {
                if (!disposable.isDisposed()) disposable.dispose();
            }
        });


    }


    private Disposable bindImageToNetworkState(Activity activity) {


        ImageView imageView = imageView(activity);


        if (imageView == null) return Disposables.disposed();


        return new OnNetworkChangedListener()
                .apply((AppCompatActivity) activity)
                .map(connected -> connected ? R.color.colorAccent : R.color.v3_gray_accents)
                .map(this::parseColor)
                .map(ColorStateList::valueOf)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(tint -> ImageViewCompat.setImageTintList(imageView, tint), this::error);
    }


    private ImageView imageView(Activity activity) {
        return AnnotationRetriever.with(NetworkStateListenerImage.class)
                .apply(activity)
                .flatMapMaybe(annotation -> Objects.maybe(annotation.value()))
                .firstElement()
                .map(activity::findViewById)
                .cast(ImageView.class)
                .doOnError(this::error)
                .onErrorResumeNext(Maybe.empty())
                .blockingGet();
    }




}

  • AnnotationRetriever is a Functional Class that checks for the Availability of the annotation and returns an Observable holding that annotation if present, or an empty Observable if it is not there ... it is a way to chain the if /else calls into an RxJava Stream, no magic here ... just consider that if there is no annotation present, all the written code will not execute
  • parseColor() is a default method in Resources interface, which takes care of parsing color resources
  • error() is a default method in Logger interface, which safely logs errors to Crashlytics
  • Objects.maybe() is a function that checks if it's parameter is not null, it will return as Maybe.just(), else it will return Maybe.empty() ... this function is there because RxJava does not accept null parameters even for it's Maybe type
  • This function first finds the ImageView with the ID that is passed to the @NetworkStateListenerImage annotation, and then it listens to the Rx Subject returned by the OnNetworkChangedListener, if it returned true, it changes the tint of the image to the accent color tint, else it changes the tint to light gray
  • And also using LifecycleObserver interface, this function stops listening to changes when the Activity is Destroyed
  • Notice that if the Annotation is not present, nothing will happen

4- Use the Annotation Scanner into an ActivityLifecycleCallbacks implementer

Now we need to execute our code in the Annotated Activity's onCreate(), so we should invoke it in an ActivityLifecycleCallback's Implemnter

public class AnnotationScanners implements Application.ActivityLifecycleCallbacks {


    private static final NetworkStateScanner NETWORK_STATE_SCANNER = new NetworkStateScanner();


    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        NETWORK_STATE_SCANNER.accept(activity);
    }


    ...


}

And Register it in our Application's onCreate()

class MyApplication extends Application {


    @Override
    public void onCreate() {
        super.onCreate();
        ...
        registerActivityLifecycleCallbacks(new AnnotationScanners());
    }
    
}

5- You are good to go

Now for Any Activity that you want to make it Network aware, you need to put the annotation with a valid ImageView ID, and that's it

@NetworkStateListenerImage(R.id.mainToolbarImageView)
class MainActivity extends AppCompatActivity {
    ...    
}


@NetworkStateListenerImage(R.id.settingsToolbarImageView)
class SettingsActivity extends AppCompatActivity {
    ...
}

No Base classes, no boiler plate code, no interfaces, just putting a single line annotation can execute as complex logic as you want

I did not talk about FragmentLifecycleCallbacks, they are very similar to ActivityLifecycleCallbacks, but they are registered in the Fragment Manager, not in the Application class, so to register them you need to do this in the ActivityLifecycleCallbacks implementer and do it in the onActivityCreated() method, and invoke the following line :

activity.getFragmentManager().registerFragmentLifecycleCallbacks(...);

That way, every Activity will register the FragmentLifecycleCallbacks implementer in it's Fragment Manager, so all the fragments will be affected by your implementer


Hisham Bakr

Staff Software Engineer - Mobile at Talabat | Delivery Hero

4 年

Great article Ahmed Adel Ismail . We can experiment with newrelic screen name. I have a question, examples in the article mention compile-time values like activity title, image view id. What about dynamic values like user id, selected list item position,.. etc? Will annotation help in that scenario or there is a technical limitation?

回复
Malek Kamel

Mobile Software Engineer | Open Source Contributor

5 年

Great article ??

要查看或添加评论,请登录

Ahmed Adel Ismail的更多文章

  • Sharing data across multiple Mobile Squads - with examples

    Sharing data across multiple Mobile Squads - with examples

    Earlier I shared an article suggesting a solution to a common problem with teams following the "Spotify Model", which…

  • SDD - Squad Driven Design

    SDD - Squad Driven Design

    Working in multiple big teams I've found that we are always trying to apply our known best practices in software, but…

    4 条评论
  • Easier Testing with MVVM, MVI, MVP and Kotlin Multiplatform

    Easier Testing with MVVM, MVI, MVP and Kotlin Multiplatform

    Before we start, this article requires basic knowledge about the following topics : Clean Architecture Unit Testing…

    10 条评论
  • Android - A Cleaner Clean Architecture

    Android - A Cleaner Clean Architecture

    It has been a while now since Clean Architecture was out, and even many of us started embracing hexagonal (ports and…

    10 条评论
  • Beyond Functional Programming

    Beyond Functional Programming

    In the Android industry, lately functional programming was the all new stuff to learn, RxJava, Kotlin, and the whole…

    7 条评论
  • Dependency Injection in Clean Architecture

    Dependency Injection in Clean Architecture

    After Google's Opinionated Guide to Dependency Injection Video, Google made a clear statement that they want developers…

    18 条评论
  • MVI Pattern For Android In 4 Steps

    MVI Pattern For Android In 4 Steps

    Lately I wrote an article about MVI pattern, but as we are facing new problems every day and face more use-cases, we…

    7 条评论
  • Agile - Moving Fast

    Agile - Moving Fast

    We always here about Agile, and think about which methodology do we use, what practices do we have, team velocity…

    1 条评论
  • Kotlin Unit Testing with Mockito

    Kotlin Unit Testing with Mockito

    I've always believed that, if the code is designed to be tested, we wont need any testing framework or library ..

    17 条评论
  • MVI - Model View Intent simplified

    MVI - Model View Intent simplified

    I have been searching for a proper resource to explain the MVI pattern, but every time I get hit with Hannes Dorfmann…

    4 条评论

社区洞察

其他会员也浏览了