Friday, December 20, 2013

Generics vs. AndroidAnnotations Android

Hi there!

In the last post i've shown the advantages of AndroidAnnotations against the tratidional instantiation way from Androd using castings while using the method findById(). AndroidAnnotatations are fine, but if for some reason (additional depencency, License or somethign else) you can't use it, there is an alternative to it. Generics solves this problem in my opinion also very elegant.

Pre-Conditions

Do the steps of post AndroidAnnotations and come back to this post when you are done.

BaseActivity with generics

Create the following class in your sample project (Attention: for some reason i don't know, while inserting code snippets here, generic types are not displayed correctly. For this reason i copied the generics like a String "T extends View". Please remove the Strings from it after copying):

import android.app.Activity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.View.OnTouchListener;

@SuppressWarnings("unchecked")
public class BaseActivity extends Activity {

    /**
     * Convenience method. Does exactly the same as {@link BaseActivity#initById(Class, int)}
     * @param safetyCheck the class type to be returned
     * @param id the defined R.id... in the layout xml file
     * @return a reference of the specified class type.
     */
    public <"T extends View"> T getReferenceById(Class safetyCheck, final int id) {
        return initById(safetyCheck, id);
    }

    /**
     * Initializes views over id's.
     * @param safetyCheck the class type to be returned
     * @param id the defined R.id... in the layout xml file
     * @return a reference of the specified class type.
     */
    public <"T extends View"> T initById(Class safetyCheck, final int id) {
        return (T) findViewById(id);
    }

    /**
     * Initializes views over id's with onClickListeners.
     * @param id the defined R.id... in the layout xml file
     * @param callback a concrete implementation of {@link OnClickCallback}
     * @return a reference of the specified class type in {@link OnClickCallback}.
     */
    public <"T extends View"> T initWithListener(final int id, final OnClickCallback callback) {
        T type = (T) findViewById(id);
        type.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                callback.onClickPerformed((T) view);
            }
        });
        return type;
    }

    /**
     * Initializes views over id's with onTouchListeners.
     * @param id the defined R.id... in the layout xml file
     * @param callback a concrete implementation of {@link OnTouchCallback}
     * @return a reference of the specified class type in {@link OnTouchCallback}.
     */
    public <"T extends View"> T initWithListener(final int id, final OnTouchCallback callback) {
        T type = (T) findViewById(id);
        type.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent event) {
                return callback.onTouchPerfomed((T) view, event);
            }

        });
        return type;
    }

    /**
     * Initializes views over id's with onLongClickListeners.
     * @param id the defined R.id... in the layout xml file
     * @param callback a concrete implementation of {@link OnLongClickCallback}
     * @return a reference of the specified class type in {@link OnLongClickCallback}.
     */
    public <"T extends View"> T initWithListener(final int id, final OnLongClickCallback callback) {
        T type = (T) findViewById(id);
        type.setOnLongClickListener(new OnLongClickListener() {
            @Override
            public boolean onLongClick(View view) {
                return callback.onLongClickPerformed((T) view);
            }
        });
        return type;
    }

    /**
     * Implement this interface and add an instance of it to the method
     * {@link BaseActivity#initWithListener(int, Callback)} while initializing your components
     * (Views)
     * @author Ricardo Ferreira
     * @since 20/12/2013
     * @version 1.0
     */
    public interface OnClickCallback<"T extends View"> {
        void onClickPerformed(T view);
    }
    /**
     * Implement this interface and add an instance of it to the method
     * {@link BaseActivity#initWithListener(int, Callback)} while initializing your components
     * (Views)
     * @author Ricardo Ferreira
     * @since 20/12/2013
     * @version 1.0
     */
    public interface OnLongClickCallback<"T extends View"> {
        boolean onLongClickPerformed(T view);
    }
    /**
     * Implement this interface and add an instance of it to the method
     * {@link BaseActivity#initWithListener(int, Callback)} while initializing your components
     * (Views)
     * @author Ricardo Ferreira
     * @since 20/12/2013
     * @version 1.0
     */
    public interface OnTouchCallback<"T extends View"> {
        boolean onTouchPerfomed(T view, MotionEvent event);
    }

}


Modify your MainActivity

Copy paste the next code segment into your MainActiviy. (Attention: for some reason i don't know, while inserting code snippets here, generic types are not displayed correctly. For this reason i copied the generics like a String "T extends View". Please remove the Strings from it after copying):

import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends BaseActivity {

    private OnClickCallback<"Button"> btnClickCallback = new ButtonClickCallback();

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

        initById(TextView.class, R.id.textView1);
        initWithListener(R.id.button1, btnClickCallback);
        initWithListener(R.id.button2, btnClickCallback);
        initWithListener(R.id.button3, btnClickCallback);
        initWithListener(R.id.button4, btnClickCallback);
        initWithListener(R.id.button5, btnClickCallback);
        initWithListener(R.id.button6, btnClickCallback);
        initWithListener(R.id.button7, btnClickCallback);
        initWithListener(R.id.button8, btnClickCallback);
        initWithListener(R.id.button9, btnClickCallback);
        initWithListener(R.id.button10, btnClickCallback);
        initWithListener(R.id.button11, btnClickCallback);
        initWithListener(R.id.button12, btnClickCallback);
        initWithListener(R.id.button13, btnClickCallback);
        initWithListener(R.id.button14, btnClickCallback);
        initWithListener(R.id.button15, btnClickCallback);

    }

    private class ButtonClickCallback implements OnClickCallback<"Button"> {
        @Override
        public void onClickPerformed(Button type) {
            switch (type.getId()) {
            case R.id.button1:
                // returns a Button... do something useful with... if you need to...
                getReferenceById(Button.class, R.id.button1); 
                handleClick();
                break;
            case R.id.textView1:
                // returns a TextView... do something useful with... if you need to...
                getReferenceById(TextView.class, R.id.textView1); 
                handleClick();
                break;
            default:
                handleClick();
                break;
            }
        }
    }

    private void handleClick() {
        Toast.makeText(this, "Generics are even cooler!", Toast.LENGTH_SHORT).show();
    }

}


That's all. Hope you like it.

😱👇 PROMOTIONAL DISCOUNT: BOOKS AND IPODS PRO ðŸ˜±ðŸ‘‡

Be sure to read, it will change your life!
Show your work by Austin Kleonhttps://amzn.to/34NVmwx

This book is a must read - it will put you in another level! (Expert)
Agile Software Development, Principles, Patterns, and Practiceshttps://amzn.to/30WQSm2

Write cleaner code and stand out!
Clean Code - A Handbook of Agile Software Craftsmanship: https://amzn.to/33RvaSv

This book is very practical, straightforward and to the point! Worth every penny!
Kotlin for Android App Development (Developer's Library): https://amzn.to/33VZ6gp

Needless to say, these are top right?
Apple AirPods Pro: https://amzn.to/2GOICxy

😱👆 PROMOTIONAL DISCOUNT: BOOKS AND IPODS PRO ðŸ˜±ðŸ‘†

Wednesday, December 18, 2013

Write less code with AndroidAnnotations and increase maintainability and readability

Hi there!

Today i'm gonna show how to write less code but simultaneously improve maintainability and readability of your android code. I was so impressed that I wanted to try myself, how good and how easy it really is. In this tuturial i will only show the benefits of AndroidAnnotations and not how to install it. But don't worry. If you decide to use it (and i'm sure you'll will :) there is no magic on how to install it. In this site you'll get all the information you need: AndroidAnnotations

The sample project

We will endup with something like this bellow to show to you the power and simplicity of AndroidAnnotatios. 




MainActivity

Create a simple project and dont change the default names while creating the project. In the folder res>layout>activity_main.xml copy/paste the following code snippet inside of it.




    

    


Initializing the components 

If you initialize the components in the tradional android way, you would end up with a class like this:

package com.treslines.androidannotations;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends Activity implements OnClickListener {

    private TextView title;
    private Button button1;
    private Button button2;
    private Button button3;
    private Button button4;
    private Button button5;
    private Button button6;
    private Button button7;
    private Button button8;
    private Button button9;
    private Button button10;
    private Button button11;
    private Button button12;
    private Button button13;
    private Button button14;
    private Button button15;

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

        title = (TextView) findViewById(R.id.textView1);
        button1 = (Button) findViewById(R.id.button1);
        button2 = (Button) findViewById(R.id.button2);
        button3 = (Button) findViewById(R.id.button3);
        button4 = (Button) findViewById(R.id.button4);
        button5 = (Button) findViewById(R.id.button5);
        button6 = (Button) findViewById(R.id.button6);
        button7 = (Button) findViewById(R.id.button7);
        button8 = (Button) findViewById(R.id.button8);
        button9 = (Button) findViewById(R.id.button9);
        button10 = (Button) findViewById(R.id.button10);
        button11 = (Button) findViewById(R.id.button11);
        button12 = (Button) findViewById(R.id.button12);
        button13 = (Button) findViewById(R.id.button13);
        button14 = (Button) findViewById(R.id.button14);
        button15 = (Button) findViewById(R.id.button15);
        
        button1.setOnClickListener(this);
        button2.setOnClickListener(this);
        button3.setOnClickListener(this);
        button4.setOnClickListener(this);
        button5.setOnClickListener(this);
        button6.setOnClickListener(this);
        button7.setOnClickListener(this);
        button8.setOnClickListener(this);
        button9.setOnClickListener(this);
        button10.setOnClickListener(this);
        button11.setOnClickListener(this);
        button12.setOnClickListener(this);
        button13.setOnClickListener(this);
        button14.setOnClickListener(this);
        button15.setOnClickListener(this);

    }

    @Override
    public void onClick(View view) {
        final int id = view.getId();
        switch (id) {

        case R.id.button1:
            handleClick();
            break;
        case R.id.button2:
            handleClick();
            break;
        case R.id.button3:
            handleClick();
            break;
        case R.id.button4:
            handleClick();
            break;
        case R.id.button5:
            handleClick();
            break;
        case R.id.button6:
            handleClick();
            break;
        case R.id.button7:
            handleClick();
            break;
        case R.id.button8:
            handleClick();
            break;
        case R.id.button9:
            handleClick();
            break;
        case R.id.button10:
            handleClick();
            break;
        case R.id.button11:
            handleClick();
            break;
        case R.id.button12:
            handleClick();
            break;
        case R.id.button13:
            handleClick();
            break;
        case R.id.button14:
            handleClick();
            break;
        case R.id.button15:
            handleClick();
            break;

        default:
            break;
        }

    }
    
    private void handleClick(){
        Toast.makeText(this, "AndroidAnnotations are cool!", Toast.LENGTH_SHORT).show();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
}



The benefits of AndroidAnnotations

If you do the same thing with AndroidAnnotations we endup with something soooooo coooool like this ;)


package com.treslines.withandroidannotations;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import com.googlecode.androidannotations.annotations.Click;
import com.googlecode.androidannotations.annotations.EActivity;
import com.googlecode.androidannotations.annotations.ViewById;

@EActivity(R.layout.activity_main)
public class MainActivity extends Activity {

    @ViewById(R.id.textView1) protected TextView title;
    @ViewById(R.id.button1) protected Button button1;
    @ViewById(R.id.button2) protected Button button2;
    @ViewById(R.id.button3) protected Button button3;
    @ViewById(R.id.button4) protected Button button4;
    @ViewById(R.id.button5) protected Button button5;
    @ViewById(R.id.button6) protected Button button6;
    @ViewById(R.id.button7) protected Button button7;
    @ViewById(R.id.button8) protected Button button8;
    @ViewById(R.id.button9) protected Button button9;
    @ViewById(R.id.button10) protected Button button10;
    @ViewById(R.id.button11) protected Button button11;
    @ViewById(R.id.button12) protected Button button12;
    @ViewById(R.id.button13) protected Button button13;
    @ViewById(R.id.button14) protected Button button14;
    @ViewById(R.id.button15) protected Button button15;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // NO NEED TO INITIALIZE COMPONENTS ANYMORE
        // ONLY 62 LINES AGAINST 139 LINES (2 TIMES LESS CODE)
        // LESS COPY/PASTE ERROR
    }

    // No switch-cases anymore...
    // Handle clicks together or independently ...
    // No need to implement onClickListener anymore... 
    @Click({R.id.button1,R.id.button2,R.id.button3}) void onButton1Clicked() {handleClick();};
    @Click({R.id.button4,R.id.button5,R.id.button6}) void onButton4Clicked() {handleClick();};
    @Click({R.id.button7,R.id.button8,R.id.button9}) void onButton7Clicked() {handleClick();};
    @Click({R.id.button10,R.id.button11,R.id.button12}) void onButton10Clicked() {handleClick();};
    @Click(R.id.button13) void onButton13Clicked() {handleClick();};
    @Click(R.id.button14) void onButton14Clicked() {handleClick();};
    @Click(R.id.button15) void onButton15Clicked() {handleClick();};

    private void handleClick(){
        Toast.makeText(this, "AndroidAnnotations are cool!", Toast.LENGTH_SHORT).show();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
}


That's all. Hope you like it.

😱👇 PROMOTIONAL DISCOUNT: BOOKS AND IPODS PRO ðŸ˜±ðŸ‘‡

Be sure to read, it will change your life!
Show your work by Austin Kleonhttps://amzn.to/34NVmwx

This book is a must read - it will put you in another level! (Expert)
Agile Software Development, Principles, Patterns, and Practiceshttps://amzn.to/30WQSm2

Write cleaner code and stand out!
Clean Code - A Handbook of Agile Software Craftsmanship: https://amzn.to/33RvaSv

This book is very practical, straightforward and to the point! Worth every penny!
Kotlin for Android App Development (Developer's Library): https://amzn.to/33VZ6gp

Needless to say, these are top right?
Apple AirPods Pro: https://amzn.to/2GOICxy

😱👆 PROMOTIONAL DISCOUNT: BOOKS AND IPODS PRO ðŸ˜±ðŸ‘†

How to create an awesome partial transparent dialog(like) Activity Android

Hi there!

Today i'll show, how to make the diffence in your profissional apps by designing real cool, awesome activities. In this sample i'm gonna share how we can create an activity that looks like a dialog with some partial transparent parts, which allows me to see the background of the launcher main activity.

Here is the sketch to visualize the idea behind the text:


Create a sample project

Create an Android Project and define your main activity as ExampleActivity. After that, create a second activity called PhotoPickerActivity. Don´t worry about the details. We will see it a few steps ahead. Just create it and leave it empty. Define in the layout of  ExampleActivity only a single button, add an OnClickListener to it and as soon as the button is pressed, call an action like that:

Intent intent = new Intent(getContext(), PhotoPickerActivity.class);
getActivity().startActivityForResult(intent, PhotoPickerActivity.REQUEST_CODE_OPEN_GALLERY);


Activity dialog style.

Create a style.xml in the folder: res>values like that:



    
    




Layout of PhotoPickerActivity

Create a layout called for example: activity_photo_picker.xml in folder: res>layout like that:



    

    

        

            

            
        

        

            
        
    

    




Manifest Entry

In your manifest, make sure you've added the following code snippet to it(Make sure the package name matches your own definition. In my case it is called com.treslines.ExampleActivity):




Content of PhotoPickerActivity

Copy/paste the next code section into PhotoPickerActivity.

public class PhotoPickerActivity extends Activity implements OnClickListener {

    public static final String ACTION_NAVIGATE_BACK = "ACTION_NAVIGATE_BACK";
    public static final String ACTION_PHOTO_SHOT = "ACTION_PHOTO_SHOT";
    public static final String ACTION_PHOTO_TAKEN = "ACTION_PHOTO_TAKEN";
    public static final int REQUEST_CODE_OPEN_GALLERY = 20;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_photo_picker);
        findViewById(R.id.close_view).setOnClickListener(this);
        findViewById(R.id.camera_view).setOnClickListener(this);
        inflatePhotoViewsToShow();
    }

    private void inflatePhotoViewsToShow() {
        // 1. Load photos from a service
        // 2. inflate photos in views here...
        // 3. add listener this click listener to each one
        // 4. add real data to the switch cases
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        switch (id) {

        case R.id.close_view:
            Intent navigateBack = new Intent(this, ExampleActivity.class);
            navigateBack.setAction(ACTION_NAVIGATE_BACK);
            navigateBack.setData(Uri.EMPTY); 
            setResult(Activity.RESULT_OK, navigateBack);
            finish();
            break;

        case R.id.camera_view:
            Intent photoShot = new Intent();
            photoShot.setAction(ACTION_PHOTO_SHOT);
            photoShot.setData(Uri.EMPTY); // set real data here...
            setResult(Activity.RESULT_OK, photoShot);
            finish();
            break;
            
        // photo has been taken (selected)
        default:
            Intent photoTaken = new Intent();
            photoTaken.setAction(ACTION_PHOTO_TAKEN);
            photoTaken.setData(Uri.EMPTY); // set real data here...
            setResult(Activity.RESULT_OK, photoTaken);
            finish();
            break;
        }
    }

}


OnActivityResult from ExampleActivity

In the method onActivityResult from your main class ExampleActivity, make sure you've added the content bellow:

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {

        if (resultCode == Activity.RESULT_OK) {
            
            if (requestCode == PhotoPickerActivity.REQUEST_CODE_OPEN_GALLERY) {
                if(!PhotoPickerActivity.ACTION_NAVIGATE_BACK.equals(data.getAction())){
                    if(PhotoPickerActivity.ACTION_PHOTO_SHOT.equals(data.getAction())){
                        Toast.makeText(this, "photo shot over camera", Toast.LENGTH_SHORT).show();
                    }else if(PhotoPickerActivity.ACTION_PHOTO_TAKEN.equals(data.getAction())){
                        Toast.makeText(this, "photo selected over photo picker", Toast.LENGTH_SHORT).show();
                    }
                }
            }

 }
}

That's all i hope you like it.

😱👇 PROMOTIONAL DISCOUNT: BOOKS AND IPODS PRO ðŸ˜±ðŸ‘‡

Be sure to read, it will change your life!
Show your work by Austin Kleonhttps://amzn.to/34NVmwx

This book is a must read - it will put you in another level! (Expert)
Agile Software Development, Principles, Patterns, and Practiceshttps://amzn.to/30WQSm2

Write cleaner code and stand out!
Clean Code - A Handbook of Agile Software Craftsmanship: https://amzn.to/33RvaSv

This book is very practical, straightforward and to the point! Worth every penny!
Kotlin for Android App Development (Developer's Library): https://amzn.to/33VZ6gp

Needless to say, these are top right?
Apple AirPods Pro: https://amzn.to/2GOICxy

😱👆 PROMOTIONAL DISCOUNT: BOOKS AND IPODS PRO ðŸ˜±ðŸ‘†

Saturday, December 7, 2013

How to identify design pattern before coding?

Hi there!

In this post we will learn how to identify design pattern before coding! Isn't it exciting? As you probabilly know, i'm addicted to design patterns. I've read a lot of good books and probabilly as you, my biggest problem was always the identification of cases/scenarios in which i could use them.

There are great books out there, but none of them explains in a simple way, how to identify it. Most of them says: "It depends on the usecase or it depends on the dev's expirience" In fact the expirience (years that you have been working on the software business) may help you identifying the right patterns to be used in some default scenarios, but can't we learn it a little bit faster? Do we have to wait years to get it done right?

Motivated by those questions, i've been trying to identify the patterns by the last mobile project i made. I was very surprised how simple it can be and how much better understandable, extensible and readable my end project was. All you need to do is to sketch it first. Really!!! That's all. Don't worry, we will see the whole process step by step. :-)

The Patterns

In this tutorial we will learn how i was able to identify a bunch of patterns before starting coding (Please note: In this tutorial i will not explain why a specific pattern is good or not. I will assume you know already design pattern and are interested in see how/when to apply it) In my project i was able to identify the following patterns:

  • Command Pattern
  • State Pattern
  • Adapter Pattern
  • Null Object Pattern
  • Singleton Pattern
  • Observer Pattern (Listeners)
  • Template Method Pattern

The Challange

Ok, let's get to an apparently simple, but real project. Imagine you have the following challange:

You have 4 buttons on a toolbar on the bottom of your application. Every button starts its own animation  when it is tapped (let's say it is moved upward to show its activity) and after its animation is done, a second animation starts. (let's say a column is slided out showing icons on it). Can you imagine it? something like this:

Sketch

The requirements

Ok, let's move on. Here are futher requirements:
  • Every animation has to be synchronized which means, if an animation is running, none of the other animations can be started, till it ends. 
  • After every transition (animation) we should have the possibility of excuting some actions if we need. (let's say play a sound or execute a desired action)
  • Every button must be locked to each other. This means, if i press and hold one button, i can't fire another action by pressing anoter button.
  • If we touch the screen and an animation is showing already, this animation will slide back and a defined dafault button will be selected. (will be moved a little bit upward)

How can i identify the patterns now?

Know that we have sketched the scenarios and have listed the requirements, how can we identify proper pattern to use in this case?

Well, what i have done was to simple imagine the five buttons as a TV-Remote control and the animations as transition states on my screen. Doing that way it was almost impossible to not see the patterns. So i thought i had commands to start the animations (Command Pattern) and i thought i had states to represent any animation's transition. (State Pattern) - The key to this was the sketch i made. Imagine: Without it, it would be almost impossible to see it.

We should give the possibility to fire actions between states. And because this should be only possibilities, i can imagine scenarios in which there will be no actions also possible right? And here they are: NullState, NullAction and NullCommand. (Null Object Pattern)

Ok let's think further. As a requirement we must ensure singularity to any button. So i automatically see the picture of a lock, because there should be only one way to start actions. In that case we need a global accessible but unique flag to control that. (Synchronized Singleton Pattern)

And last but not least, as we will be dealing with a lot of action listeners and because of the button's synchronization, it would be necessary to implement a lot of individual Listeners for every button. If we do that way, we would end up with a bunch of duplicated code lines. For this reason we should implement an adapter of the TouchListener in which we place the lock logic in central unique place (Template Method Pattern), making it available for all buttons in the aplication. (Adapter Pattern). So i ended up with a simple but very usefull class diagram like that bellow which i used to define the app's architecture (structural part of it):

initial class diagram

Conclusion

In my opinion at least two things are indispensable in the process of figuring out what design pattern to apply:
  • a sketch 
  • as many requirements as available

Share your expirience

If you know other simple, practical ways on how to find out which design pattern to use, please feel free to contact me and share your thoughts.  It would be great to se the usage of other pattern in practise. (real projects) like i did. That's all. hope you like it.

😱👇 PROMOTIONAL DISCOUNT: BOOKS AND IPODS PRO ðŸ˜±ðŸ‘‡

Be sure to read, it will change your life!
Show your work by Austin Kleonhttps://amzn.to/34NVmwx

This book is a must read - it will put you in another level! (Expert)
Agile Software Development, Principles, Patterns, and Practiceshttps://amzn.to/30WQSm2

Write cleaner code and stand out!
Clean Code - A Handbook of Agile Software Craftsmanship: https://amzn.to/33RvaSv

This book is very practical, straightforward and to the point! Worth every penny!
Kotlin for Android App Development (Developer's Library): https://amzn.to/33VZ6gp

Needless to say, these are top right?
Apple AirPods Pro: https://amzn.to/2GOICxy

😱👆 PROMOTIONAL DISCOUNT: BOOKS AND IPODS PRO ðŸ˜±ðŸ‘†