Skip to main content

Tinder mockup, Retrofit, and Gson



    It's ironic that the larger a company becomes, even with all the resources it obtains, the slower and less influential it is. This is why larger company's tend to buy out the newest start-ups. But once owned by a larger company, the company hardly innovates and is very slow to release updates (perhaps due to the internal bureaucracy). Sometimes this results in a sturdy yet not so exciting product. However, sometimes I've noticed it results in a worse product (maybe due to them overly complicating their product in an attempt to accommodate as much people as possible). I'm not sure if this is the case with Tinder (I don't know if they were bought out by any larger company) but I know they have become very popular (maybe?) and I have noticed a seemingly steady decline in the quality of their product.

    Over time, I have noticed many annoying problems with their app. I have provided feedback to them via the app's feedback option and have even speculated as what can be the cause of their issue and a possible solution how to fix it. Of course I didn't receive a response (they might not even check their feedback or they just might not have the time to get back to everyone, no worries just thought I'd help). But as time went on and updates rolled out, I was still facing a lot of these same issues (mostly UI problems) which somewhat annoyed me. So, I decompiled their APK and had a look at their source code. It was hard to get a complete picture of what was going since the code was obviously obfuscated but I did see enough to get a gist.

    It seemed that they had Fragments for every little View type and interaction which could be what was causing most of the problems (they must not be properly handling the switching of these fragments). One issue I noticed with the app is that sometimes the images don't load. I was surprised to find that they use the Picasso library because that is a really good library (I use it often and never faced this issue), so it must be an issue with how they handle using the library. I might not been able to locate the exact problems with their code but all this snooping around got me thinking how would I go about creating the client side app. So I created it. 

    I just hacked up a quick and crude version of the Tinder app today. Obviously theirs is more complex (which creates more room for errors) but I just wanted to hack up the gist of it. The code is mainly just UI and structural. I didn't code the server side logic and I didn't go into great detail; theres no transition animations or anything of that nature (I don't want to spend more than one weekend day on this). I just wanted to create a quick mockup of the main experience the app offers. So here's the logic flow:

User opens app >

    Splash Activity appears, uses Facebook SDK to check if the user is signed in >

        Signed In? >

            Yes >

                 MainActivity

             No >

                  LoginActivity

    The MainActivity is the Activity that contains the swipeable cards. I used the Facebook SDK for authentication but since this is just a mockup app I'm not registering an App ID and I'm not coding the server-side logic, so I comment blocked that logic out and just passed over to the MainActivity. The MainActivity is an Activity with a DrawerLayout to slide open for the side menu, a custom Toolbar with a TabLayout for the tabs, and a ViewPager so you can slide between the different Fragments with the TabLayout in the Toolbar. There's just two Fragments the one containing the cards and the one containing the matches. Luckily there's already a library for the cards so I don't have to code the custom Views and Adapters (which shouldn't be too hard just time consuming). And I'll just use FloatingActionButtons from this library for the dislike, like, and superlike buttons since they're circular and have an elevation to them. I'll also being using Retrofit and Gson for handling the REST requests to the https://randomuser.me/ service that will randomly populate the cards with values and pictures.

All the code can be found here.

First lets have a look at our MainActivity (which I call HomeActivity) layout:


    <?xml version="1.0" encoding="utf-8"?>
    <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/drawer_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- The main content view -->
        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <include
                android:id="@+id/toolbar"
                layout="@layout/tab_toolbar" />

            <android.support.v4.view.ViewPager
                xmlns:android="http://schemas.android.com/apk/res/android"
                android:id="@+id/pager"
                android:layout_width="match_parent"
                android:layout_height="0px"
                android:layout_weight="1" />

        </LinearLayout>

        <!-- The navigation drawer -->
        <android.support.design.widget.NavigationView
            android:id="@+id/navigation_view"
            android:layout_height="match_parent"
            android:layout_width="wrap_content"
            android:layout_gravity="start"
            app:menu="@menu/drawer" />

    </android.support.v4.widget.DrawerLayout>


    As we can see the layout is pretty simple and straightforward. The root view is a DrawerLayout so we can have a slide out menu. The first child of the DrawerLayout is the actual main view, here we have a LinearLayout ViewGroup which has a Toolbar (this is "removed" from the "main" view and set as the ActionBar later in code) and a ViewPager as children. The second child of the DrawerLayout is a NavigationView from the design support library, this view will be the view for the slide out menu, it'll contain a list of menu options as well as a header view displaying the user's information. Now, the Activity class:


public class HomeActivity extends AppCompatActivity {
    private static final String TAG = HomeActivity.class.getSimpleName();
    private Toolbar toolbar;
    private DrawerLayout drawerLayout;
    private ActionBarDrawerToggle drawerToggle;
    private NavigationView navigationView;
    private View headerView;
    private ViewPager viewPager;
    private TabLayout tabLayout;

    @Override
    @SuppressWarnings("deprecation")
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.home);
        toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        navigationView = (NavigationView) findViewById(R.id.navigation_view);
        navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(MenuItem menuItem) {
                menuItem.setChecked(false);
                drawerLayout.closeDrawers();
                //Add logic to perform the appropriate action depending on what item was selected
                switch(menuItem.getItemId()){
                    case R.id.discover_settings:
                        return true;
                    case R.id.app_settings:
                        return true;
                    case R.id.help:
                        return true;
                    case R.id.feedback:
                        return true;
                    case R.id.tinder_plus:
                        return true;
                }
                return false;
            }
        });
        headerView = LayoutInflater.from(this).inflate(R.layout.navigation_header, navigationView, false);
        headerView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //TODO Load the Users profile
            }
        });
        navigationView.addHeaderView(headerView);
        //TODO Add logic to display the users information into the header view
        drawerToggle = new ActionBarDrawerToggle(
                this,                  /* host Activity */
                drawerLayout,         /* DrawerLayout object */
                toolbar,  /* nav drawer icon to replace 'Up' caret */
                R.string.drawer_open,  /* "open drawer" description */
                R.string.drawer_close  /* "close drawer" description */
        ) {
            /**
             * Called when a drawer has settled in a completely closed state.
             */
            public void onDrawerClosed(View view) {
                super.onDrawerClosed(view);
            }

            /**
             * Called when a drawer has settled in a completely open state.
             */
            public void onDrawerOpened(View drawerView) {
                super.onDrawerOpened(drawerView);
            }
        };
        // Set the drawer toggle as the DrawerListener
        drawerLayout.addDrawerListener(drawerToggle);
        //Set up the tabs and swipeable views
        viewPager = (ViewPager) findViewById(R.id.pager);
        ScreenSlidePagerAdapter adapter = new ScreenSlidePagerAdapter(getSupportFragmentManager(), this);
        viewPager.setAdapter(adapter);
        tabLayout = (TabLayout) toolbar.findViewById(R.id.sliding_tabs);
        tabLayout.setupWithViewPager(viewPager);
        //Set up the icons
        for (int i = 0; i < tabLayout.getTabCount(); i++) {
            switch(i){
                default:
                case 0:
                    tabLayout.getTabAt(i).setIcon(R.drawable.tinder_icon);
                    break;
                case 1:
                    tabLayout.getTabAt(i).setIcon(R.drawable.speech_bubbles_gray);
                    break;
            }
        }
        getSupportActionBar().setDisplayShowHomeEnabled(false);
        getSupportActionBar().setDisplayShowTitleEnabled(false);
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        // Sync the toggle state after onRestoreInstanceState has occurred.
        drawerToggle.syncState();
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        drawerToggle.onConfigurationChanged(newConfig);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Pass the event to ActionBarDrawerToggle, if it returns
        // true, then it has handled the app icon touch event
        if (drawerToggle.onOptionsItemSelected(item)) {
            return true;
        }
        // Handle your other action bar items...
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onBackPressed() {
        if(drawerLayout.isDrawerOpen(GravityCompat.START)){
            drawerLayout.closeDrawer(GravityCompat.START);
        }else{
            super.onBackPressed();
        }
    }


    private class ScreenSlidePagerAdapter extends FragmentPagerAdapter {
        private Context context;

        public ScreenSlidePagerAdapter(FragmentManager fm, Context context){
            super(fm);
            this.context = context;
        }

        @Override
        public Fragment getItem(int position) {
            switch(position){
                default:
                case 0:
                    return CardFragment.newInstance();
                case 1:
                    return MatchFragment.newInstance();
            }
        }

        @Override
        public int getCount() {
            return 2;
        }

        @Override
        public CharSequence getPageTitle(int position) {
           return "";
        }

    }


}


    This class is mainly just structural code. In the overrided onCreate method we set the Toolbar as the support action bar with the setSupportActionBar(toolbar) method call. We set up an item selected listener on our NavigationView. We set a custom view as our header view to the NavigationView. We add a DrawerListener to the DrawerLayout just incase we want to listen to events. We sync the ViewPager with a FragmentPagerAdapter and our custom Toolbar which contains a TabLayout. And the rest of the code in the class is just handling events and such. The FragmentPagerAdapter internal class that we set up is what changes the view to the appropriate Fragment. So, now we can set up the Fragments. We'll focus on the main Fragment, CardFragment:


public class CardFragment extends Fragment {
    private com.andtinder.view.CardContainer cardContainer;
    private UserCardAdapter adapter;
    private FloatingActionButton dislike;
    private FloatingActionButton superlike;
    private FloatingActionButton like;

    public static CardFragment newInstance(){
        return new CardFragment();
    }

    @Override
    @SuppressWarnings("deprecation")
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
        View v = inflater.from(container.getContext()).inflate(R.layout.card_fragment, container, false);
        cardContainer = (CardContainer) v.findViewById(R.id.card_container);
        cardContainer.setOrientation(Orientations.Orientation.Disordered);
        adapter = new UserCardAdapter(getContext());
        cardContainer.setAdapter(adapter);
        dislike = (FloatingActionButton) v.findViewById(R.id.dislike);
        superlike = (FloatingActionButton) v.findViewById(R.id.superlike);
        like = (FloatingActionButton) v.findViewById(R.id.like);
        //set up these fields in code because the library doesn't properly handle the xml custom attributes
        dislike.setColorNormal(getContext().getResources().getColor(R.color.tinder_red));
        dislike.setColorPressed(getContext().getResources().getColor(R.color.tinder_red));
        dislike.setIconDrawable(getContext().getResources().getDrawable(R.drawable.tinder_dislike));
        dislike.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //TODO add logic
            }
        });
        superlike.setColorNormal(getContext().getResources().getColor(R.color.tinder_blue));
        superlike.setColorPressed(getContext().getResources().getColor(R.color.tinder_blue));
        superlike.setIconDrawable(getContext().getResources().getDrawable(R.drawable.superlike));
        superlike.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //TODO add logic
            }
        });
        like.setColorNormal(getContext().getResources().getColor(R.color.tinder_green));
        like.setColorPressed(getContext().getResources().getColor(R.color.tinder_green));
        like.setIconDrawable(getContext().getResources().getDrawable(R.drawable.tinder_like));
        like.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //TODO add logic
            }
        });
        return v;
    }

}


Gson Deserialization

    So we're using randomuser.me to feed us random user information to display in our cards. The result comes back in the form of JSON (which is common). We're using Retrofit to make an interface for our HTTP API method call (randomuser.me/api). Retrofit comes with support for add on modules that can parse different forms of data, one of them being Gson (Google's JSON library). The Gson library can deserialize in two ways: a POJO (Plain Old Java Object) hierarchy mimicking the JSON response format (which it uses reflection to properly deserialize the JSON into the objects) and a custom JsonDeserializer interface you can provide to the Gson builder. I went with the former since its pretty straightforward. Let's have a look at the JSON response format we're dealing with here:


{
  results: [{
    user: {
      gender: "female",
      name: {
        title: "ms",
        first: "manuela",
        last: "velasco"
      },
      location: {
        street: "1969 calle de alberto aguilera",
        city: "la coruña",
        state: "asturias",
        zip: "56298"
      },
      email: "manuela.velasco50@example.com",
      username: "heavybutterfly920",
      password: "enterprise",
      salt: ">egEn6YsO",
      md5: "2dd1894ea9d19bf5479992da95713a3a",
      sha1: "ba230bc400723f470b68e9609ab7d0e6cf123b59",
      sha256: "f4f52bf8c5ad7fc759d1d4156b25a4c7b3d1e2eec6c92d80e508aa0b7946d4ba",
      registered: "1303647245",
      dob: "415458547",
      phone: "994-131-106",
      cell: "626-695-164",
      DNI: "52434048-I",
      picture: {
        large: "http://api.randomuser.me/portraits/women/39.jpg",
        medium: "http://api.randomuser.me/portraits/med/women/39.jpg",
        thumbnail: "http://api.randomuser.me/portraits/thumb/women/39.jpg",
      },
      version: "0.6"
      nationality: "ES"
    },
    seed: "graywolf"
  }]
}


    As we can see what we are returned is an object (within the "{}" brackets) with a "results" field. This "results" field is an array (within the "[]" brackets) of objects. Each of these objects has two fields "user" and "seed". The "seed" field is just a String and the "user" field is an object. We're only interested in some of the "user" object fields but as you can see some of those are also objects, such as the "name" field. So let's create our POJO's:


public class UserResult {
    private List<ResultObject> results;

    public UserResult(){

    }

    public UserResult(List<ResultObject> results){
        this.results = results;
    }

    public List<ResultObject> getResults() {
        return results;
    }

    public void setResults(List<ResultObject> results) {
        this.results = results;
    }

}


    The above class is the object that is returned from the API call which has the "results" field. The "results" field is an array of objects so in our Java class it's a List of type ResultObject which is the next POJO we define.


public class ResultObject {
    private User user;
    private String seed;

    public ResultObject(){

    }

    public ResultObject(User user, String seed){
        this.user = user;
        this.seed = seed;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public String getSeed() {
        return seed;
    }

    public void setSeed(String seed) {
        this.seed = seed;
    }

}


    The code above follows the same pattern as specified before. And finally our User object which contains some internal classes representing its fields.


public class User {
    private String gender;
    private Name name;
    private String email;
    private String username;
    private Picture picture;

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public Name getName() {
        return name;
    }

    public void setName(Name name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Picture getPicture() {
        return picture;
    }

    public void setPicture(Picture picture) {
        this.picture = picture;
    }

    public static class Name{
        private String title;
        private String first;
        private String last;

        public Name(){

        }

        public Name(String title, String first, String last){
            this.title = title;
            this.first = first;
            this.last = last;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getFirst() {
            return first;
        }

        public void setFirst(String first) {
            this.first = first;
        }

        public String getLast() {
            return last;
        }

        public void setLast(String last) {
            this.last = last;
        }

    }

    public static class Picture{
        private String large;
        private String medium;
        private String thumbnail;

        public Picture(){

        }

        public Picture(String large, String medium, String thumbnail){
            this.large = large;
            this.medium = medium;
            this.thumbnail = thumbnail;
        }

        public String getLarge() {
            return large;
        }

        public void setLarge(String large) {
            this.large = large;
        }

        public String getMedium() {
            return medium;
        }

        public void setMedium(String medium) {
            this.medium = medium;
        }

        public String getThumbnail() {
            return thumbnail;
        }

        public void setThumbnail(String thumbnail) {
            this.thumbnail = thumbnail;
        }

    }


}


Retrofit

    Retrofit is a pretty amazing library that let's you turn HTTP API's into Java Interfaces. This greatly simplifies performing REST requests and such putting the HTTP methods as Java methods. Here's how it works in our case: we create a Java Interface and define the relative endpoint and parameters for each method. We then use the Retrofit class to make an instance of our Interface and then we call our method we specified and to get a Call object which we use to execute our request and get a response. Here's the Interface:


public interface UserService {
    @GET("?results=amount")
    Call<UserResult> getUsers(@Query("results") int amount);
}


    Now we can use the Retrofit class to make an implementation of our interface. Retrofit can be called synchronously or asynchronously, if you call it synchronously in Android make sure to do it off the main UI thread (I do it in an AsyncTask). Here's the relevant code from the UserCardAdapter class:


private void loadUsers(){
        new AsyncTask<Void, Void, UserResult>(){
            @Override
            protected UserResult doInBackground(Void... params) {
                try {
                    Retrofit retrofit = new Retrofit.Builder()
                            .baseUrl("http://api.randomuser.me/")
                            .addConverterFactory(GsonConverterFactory.create()) //We're using Gson to deserialize our response
                            .build();
                    UserService service = retrofit.create(UserService.class); //Creates an implementation of our interface
                    Call<UserResult> call = service.getUsers(20); //Our method call returns our Call object of type UserResult
                    return call.execute().body();//execute our call object (blocking) and the body is our UserResult object
                }catch(Exception e){
                    e.printStackTrace();
                }
                return null;
            }
            @Override
            public void onPostExecute(UserResult result){
                if(result != null) {
                    //Take the result and add the User information to cards that can be swiped
                    Drawable defaultDrawable = getContext().getResources().getDrawable(R.drawable.tinder_icon);
                    User u;
                    for (ResultObject r : result.getResults()) {
                        u = r.getUser();
                        add(new CardModel(u.getName().getFirst(), u.getPicture().getLarge(), defaultDrawable));
                    }
                }
            }
        }.execute();
    }


    And that's all there is to it, the basis of a Tinder mockup application. Obviously, Tinder has more features, server-side logic, and animations. But this is a good starting point that if I had more interest, time, and resources for this project I could continue to develop. This post isn't to undermine Tinder and the work and innovation they have done but more just a proof of concept. I often look at apps and wonder "how did they code that and how would I go about it" (and if there's an error with the app, "how could I fix it"). I apologize for the "rantiness" (I know that's not a word) of this post I just was typing this as I was coding the project and I didn't want to spend more than one weekend day on it as I have other things I need to do, so, I rushed a little and didn't spell or grammar check. Hopefully, this was fun, interesting, and insightful for someone. 

Comments

Popular posts from this blog

Face detection and live filters

Live video filters are becoming a popular trend fueled by Facebook (through their purchase of Msqrd) and Snapchat incorporating the features into their apps. These filters apply images or animations to your face using face tracking software. This technology has been around for awhile but is becoming increasingly more common due to the powerful CPU's that our mobile phones now have. Google provides an API that provides face tracking abilities through the Google Play Services library called Mobile Vision. I'm going to use their API to build a basic live filter app. The end result will look something like this:


    The bounding box wraps around the detected face and the sunglasses are the filter I chose (which is just a PNG image) which are drawn over the eyes. You could use any PNG image (with alpha for the background) you want, you will just have to adjust the layout according to where the image should be displayed. As you move your head, the box and sunglasses are redrawn…

Setting Up Connection Pooling With Elastic Beanstalk

Amazon's Elastic Beanstalk is a service which automatically scales your application when needed. It uses Amazon's Elastic Compute Cloud (EC2) instances as deployable containers which when your app requires more resources more containers will be deployed. This removes the need to manually configure your EC2 instance whenever you need more connections or resources and attempts to add simplicity to the maintenance aspect of your application. So, when you get more users of your app, your app will scale accordingly.

    Unfortunately, along with the ability to scale automatically, comes less control and configuration. Things you would normally have the ability to configure to your liking, such as your server, you no longer can. Amazon attempts to address this issue with configuration files. You can provide configuration files which can set up your server. These files are either written in JSON or the horrible format YAML. Though these files provide some level of control, you ca…

Android Guitar Tuner

Recently I created a guitar tuner application for Android that is written with pure Java (no C++ or NDK usage). The design was inspired by the Google Chrome team's guitar tuner web app using the WebAudio API. I wanted to code a version written natively for Android that didn't have to rely on a WebView, the WebAudio APIs, or server-side logic. Also, I wanted this application to be available to as many versions of Android as possible (whereas the WebAudio API may only be supported in more recent versions of WebView available only on newer flavors of Android). So, I coded it from scratch. I used a portion of the open source TarsosDSP project (their YIN algorithm) to help with the pitch detection.

    The application is available in the Google Play Store for Android: https://play.google.com/store/apps/details?id=com.chrynan.guitartuner. The project is completely open source and the code can be found on the GitHub repository: https://github.com/chRyNaN/Android-Guitar-Tuner. Fi…