Tuesday, January 26, 2016

Understanding Architecture of Nick Butcher's Plaid App - Part-6

AboutActivity.java




About screen. This displays 3 pages in a ViewPager:
  – About Plaid
  – Credit Roman for the awesome icon
  – Credit libraries


ViewPager associates each page with a key Object instead of working with Views directly. This key is used to track and uniquely identify a given page independent of its position in the adapter

A very simple PagerAdapter may choose to use the page Views themselves as key objects, returning them from instantiateItem(ViewGroup, int) after creation and adding them to the parent ViewGroup. A matching destroyItem(ViewGroup, int, Object) implementation would remove the View from the parent ViewGroup and isViewFromObject(View, Object) could be implemented as return view == object;.


21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
  static class AboutPagerAdapter extends PagerAdapter {

        private View aboutPlaid;
        @Nullable @Bind(R.id.about_description) TextView plaidDescription;
        private View aboutIcon;
        @Nullable @Bind(R.id.icon_description) TextView iconDescription;
        private View aboutLibs;
        @Nullable @Bind(R.id.libs_list) RecyclerView libsList;

        private final LayoutInflater layoutInflater;
        private final Bypass markdown;

        public AboutPagerAdapter(Context context) {
            layoutInflater = LayoutInflater.from(context);
            markdown = new Bypass(context, new Bypass.Options());
        }

        @Override
        public Object instantiateItem(ViewGroup collection, int position) {
            View layout = getPage(position, collection);
            collection.addView(layout);
            return layout;
        }

        @Override
        public void destroyItem(ViewGroup collection, int position, Object view) {
            collection.removeView((View) view);
        }

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

        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }

        private View getPage(int position, ViewGroup parent) {
            switch (position) {
                case 0:
                    if (aboutPlaid == null) {
                        aboutPlaid = layoutInflater.inflate(R.layout.about_plaid, parent, false);
                          :
                        return aboutPlaid;
                case 1:
                    if (aboutIcon == null) {
                        aboutIcon = layoutInflater.inflate(R.layout.about_icon, parent, false);
                        ButterKnife.bind(this, aboutIcon);
                        CharSequence icon0 = parent.getResources().getString(R.string.about_icon_0);
                        CharSequence icon1 = markdown.markdownToSpannable(parent.getResources()
                                .getString(R.string.about_icon_1), iconDescription, null);
                        CharSequence iconDesc = TextUtils.concat(icon0, "\n", icon1);
                        HtmlUtils.setTextWithNiceLinks(iconDescription, iconDesc);
                    }
                    return aboutIcon;
                case 2:
                    if (aboutLibs == null) {
                        aboutLibs = layoutInflater.inflate(R.layout.about_libs, parent, false);
                        ButterKnife.bind(this, aboutLibs);
                        libsList.setAdapter(new LibraryAdapter(parent.getContext()));
                    }
                    return aboutLibs;
            }
            throw new InvalidParameterException();
        }
    }


You will also find a LibraryAdapter which is just a simple recyclerview adapter for recyclerview on the third page which shows popular android opensource libraries.

Since we are doing the Architecture explanation here , we will not go into details of how the elastic effect is achieved and how awesomely that InkPageIndicator is implemented.

That info is for the Learnings from Plaid series which will definitely cover in that series :-) till that time just appreciate the effect.



Understanding Architecture of Nick Butcher's Plaid App - Part-5

Today we are going to look at how Plaid app communicates with Dribble API and that its the same mechanism with which it communicates with Designer News and Producthunt APIs. but for the purpose of this blog we are going to look at only the Dribble APIs.

This is the tree that we will discuss today.

Data->api
  • Designernews
  • Dribble
  • Producthunt     







If you open the navigation drawer and click on
  • Dribble following
  • My Dribble Shots
  • My Dribble likes

You will be asked for a login into Dribble.


Plaid app uses Retrofit to for the communication with Dribble API , why ? Just because its awesome , you will see how in this post.  Retrofit is popular library among all android developers for communicating with Rest APIs and its from SQUARE.

Model Classes : 

Shot.java - modeled from http://developer.dribbble.com/v1/shots/
User.java  - modeled from http://developer.dribbble.com/v1/users/

Response
The response will be returned as JSON and takes the following form:
{
 
"access_token" : "29ed478ab86c07f1c069b1af76088f7431396b7c4a2523d06911345da82224a0",
 
"token_type" : "bearer",
 
"scope" : "public write"
}

Images.java - modeled from field images from  http://developer.dribbble.com/v1/shots/


Retrofit endpoint specific implementation

DribbbleAuthService.java
      This service helps authenticating with Dribble API

 DribbbleService.java
     This service helps fetch the data from Dribble API once the authentication is successful



DribbleLogin.java 

Flow :   Once you press on login button

DribbleLogin.java
  public void doLogin(View view) {
        showLoading();
        dribbblePrefs.login(DribbbleLogin.this);
    }


DribblePrefs.java
 public static final String LOGIN_URL = "https://dribbble.com/oauth/authorize?client_id="
            + BuildConfig.DRIBBBLE_CLIENT_ID
            + "&redirect_uri=plaid%3A%2F%2F" + LOGIN_CALLBACK
            + "&scope=public+write+comment+upload";


 public void login(Context context) {
        Log.d("#### " , Uri.parse(LOGIN_URL).toString());
        context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(LOGIN_URL)));
    }


for Dribblelogin activity , in AndroidManifest.xml, we have  android:launchmode = "SingleTop" ,
Once we are redirected back to the activity after authentication , we use the same activity instance and redeliver the intent to same activity.

This time onNewIntent() is invoked and since we will receive the access token as part of this intent we fetch the accesstoken.  Then we authenticate with Dribble auth endpoint using the client ID / SECRET and the access token that we received. Once the authentication is successful , the logged in user is displayed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
   @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        checkAuthCallback(intent);
    }

  
private void checkAuthCallback(Intent intent) {
        if (intent != null
                && intent.getData() != null
                && !TextUtils.isEmpty(intent.getData().getAuthority())
                && DribbblePrefs.LOGIN_CALLBACK.equals(intent.getData().getAuthority())) {
            showLoading();
            getAccessToken(intent.getData().getQueryParameter("code"));
        }

    }



  private void getAccessToken(String code) {
        RestAdapter restAdapter = new RestAdapter.Builder()
                .setEndpoint(DribbbleAuthService.ENDPOINT)
                .build();

        DribbbleAuthService dribbbleAuthApi = restAdapter.create((DribbbleAuthService.class));

        dribbbleAuthApi.getAccessToken(BuildConfig.DRIBBBLE_CLIENT_ID,
                BuildConfig.DRIBBBLE_CLIENT_SECRET,
                code, "", new Callback<AccessToken>() {
                    @Override
                    public void success(AccessToken accessToken, Response response) {
                        dribbblePrefs.setAccessToken(accessToken.access_token);
                        showLoggedInUser();
                        setResult(Activity.RESULT_OK);
                        finishAfterTransition();
                    }

                    @Override
                    public void failure(RetrofitError error) {
                        Log.e(getClass().getCanonicalName(), error.getMessage(), error);
                        // TODO snackbar?
                        Toast.makeText(getApplicationContext(), "Log in failed: " + error
                                .getResponse()
                                .getStatus(), Toast.LENGTH_LONG).show();
                        showLogin();
                    }
                });
    }


private void showLoggedInUser() {
        Gson gson = new GsonBuilder()
                .setDateFormat(DribbbleService.DATE_FORMAT)
                .create();

        RestAdapter restAdapter = new RestAdapter.Builder()
                .setEndpoint(DribbbleService.ENDPOINT)
                .setConverter(new GsonConverter(gson))
                .setRequestInterceptor(new AuthInterceptor(dribbblePrefs.getAccessToken()))
                .build();

        DribbbleService dribbbleApi = restAdapter.create((DribbbleService.class));
        dribbbleApi.getAuthenticatedUser(new Callback<User>() {
            @Override
            public void success(User user, Response response) {
                dribbblePrefs.setLoggedInUser(user);
                Toast confirmLogin = new Toast(getApplicationContext());
                View v = LayoutInflater.from(DribbbleLogin.this).inflate(R.layout
                        .toast_logged_in_confirmation, null, false);
                ((TextView) v.findViewById(R.id.name)).setText(user.name);
                // need to use app context here as the activity will be destroyed shortly
                Glide.with(getApplicationContext())
                        .load(user.avatar_url)
                        .placeholder(R.drawable.ic_player)
                        .transform(new CircleTransform(getApplicationContext()))
                        .into((ImageView) v.findViewById(R.id.avatar));
                v.findViewById(R.id.scrim).setBackground(ScrimUtil.makeCubicGradientScrimDrawable
                        (ContextCompat.getColor(DribbbleLogin.this, R.color.scrim),
                                5, Gravity.BOTTOM));
                confirmLogin.setView(v);
                confirmLogin.setGravity(Gravity.BOTTOM | Gravity.FILL_HORIZONTAL, 0, 0);
                confirmLogin.setDuration(Toast.LENGTH_LONG);
                confirmLogin.show();
            }

            @Override
            public void failure(RetrofitError error) {
            }
        });
    }

AuthInterceptor.java 

A {@see RequestInterceptor} that adds an auth token to requests

 @Override
    public void intercept(RequestFacade request) {
        request.addHeader("Authorization", "Bearer " + accessToken);
    }


Use the access token to access the API.
The access token allows you to make requests to the API on a behalf of a user.
You can pass the token in the query parameters like shown above, but a cleaner approach is to include it in the Authorization header:
Authorization: Bearer ACCESS_TOKEN
For example, in curl you can set the Authorization header like this:
curl -H "Authorization: Bearer ACCESS_TOKEN" https://api.dribbble.com/v1/user


Saturday, January 23, 2016

Understanding Architecture of Nick Butcher's Plaid App - Part-4

FeedAdapter - FeedAdapter.java


Adapter for the main screen grid of items
  • holds on to an activity ref for the shared element transitions
  • It’s really just like any other recyclerview adapter with layout inflation in onCreateView () and binding of views to viewholders in onBindView()  
  • it deals with following items  : - private List<PlaidItem> items;
  • It deals with four different itemview types  
    •  private static final int TYPE_DESIGNER_NEWS_STORY = 0;
    •  private static final int TYPE_DRIBBBLE_SHOT = 1;
    •  private static final int TYPE_PRODUCT_HUNT_POST = 2;
    •  private static final int TYPE_LOADING_MORE = -1;  
This adapter initialized is initialized in HomeActivity.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
 @Bind(R.id.stories_grid) RecyclerView grid;

     private FeedAdapter adapter;


adapter = new FeedAdapter(this, dataManager, columns, PocketUtils.isPocketInstalled(this));

        grid.setAdapter(adapter);

        layoutManager = new GridLayoutManager(this, columns);

        layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {

            @Override

            public int getSpanSize(int position) {

                return adapter.getItemColumnSpan(position);

            }

        });


        grid.setLayoutManager(layoutManager);

        grid.addOnScrollListener(gridScroll);

        grid.addOnScrollListener(new InfiniteScrollListener(layoutManager, dataManager) {

            @Override

            public void onLoadMore() {

                dataManager.loadAllDataSources();

            }

        });

        grid.setHasFixedSize(true);

        grid.addItemDecoration(new GridItemDividerDecoration(adapter.getDividedViewHolderClasses(),

                this, R.dimen.divider_height, R.color.divider));

        grid.setItemAnimator(new HomeGridItemAnimator());


Its important to note that the  TYPE_LOADING_MORE is returned when the scrolled position is more than the items in  the List<PlaidItems>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  @Override
    public int getItemViewType(int position) {
        if (position < getDataItemCount()
                && getDataItemCount() > 0) {
            PlaidItem item = getItem(position);
            if (item instanceof Story) {
                return TYPE_DESIGNER_NEWS_STORY;
            } else if (item instanceof Shot) {
                return TYPE_DRIBBBLE_SHOT;
            } else if (item instanceof Post) {
                return TYPE_PRODUCT_HUNT_POST;
            }
        }
        return TYPE_LOADING_MORE;
    }

Understanding Architecture of Nick Butcher's Plaid App - Part-3

DataManager -  DataManager.java

Responsible for loading data from the various sources. Instantiating classes are responsible for
providing the {code onDataLoaded} method to do something with the data.
  • Important bit to note here is DataManager extends from BaseDatamanager which has an abstract method onDataLoded which will be called once the data is fetched from sources selected in the filter

  • DataManager constructor accepts filterAdapter and then loads data from all the filters in the adapter 

  • It also implements filtersChanged listener interface so that it gets notified when the filters change and then it can load resources of that filter 

Important bit to note here is DataManager extends from BaseDatamanager which has an abstract method
 
public abstract void onDataLoaded(List<? extends PlaidItem> data);

In HomeAcivity when the Datamanager is initialized this method is implemented

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
dataManager = new DataManager(this, filtersAdapter) {

            @Override

            public void onDataLoaded(List<? extends PlaidItem> data) {

                adapter.addAndResort(data);

                checkEmptyState();

            }

        };

dataManager.loadAllDataSources(); <--------------- This is onCreate () causes loading the data from all sources

Note that DataManager constructor accepts filterAdapter and then loads data from all the filters in the adapter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 public DataManager(Context context,

                       FilterAdapter filterAdapter) {

        super(context);

        this.filterAdapter = filterAdapter;

        setupPageIndexes();

    }


    public void loadAllDataSources() {

        for (Source filter : filterAdapter.getFilters()) {

            loadSource(filter);

        }

    }


It also implements filtersChanged listener interface so that it gets notified when the filters change and then it can load resources of that filter 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 private void loadSource(Source source) {
        if (source.active) {
            loadStarted();
            int page = getNextPageIndex(source.key);
            switch (source.key) {
                case SourceManager.SOURCE_DESIGNER_NEWS_POPULAR:
                    loadDesignerNewsTopStories(page);
                    break;
                case SourceManager.SOURCE_DESIGNER_NEWS_RECENT:
                    loadDesignerNewsRecent(page);
                    break;
                case SourceManager.SOURCE_DRIBBBLE_POPULAR:
                    loadDribbblePopular(page);
                    break;
:
:
:
}


Look at how OnDataLoaded () method is invoked so that we can notify the initializer about the finish load event

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private void loadDesignerNewsTopStories(final int page) {
        getDesignerNewsApi().getTopStories(page, new Callback<StoriesResponse>() {
            @Override
            public void success(StoriesResponse storiesResponse, Response response) {
                if (storiesResponse != null
                        && sourceIsEnabled(SourceManager.SOURCE_DESIGNER_NEWS_POPULAR)) {
                    setPage(storiesResponse.stories, page);
                    setDataSource(storiesResponse.stories,
                            SourceManager.SOURCE_DESIGNER_NEWS_POPULAR);
                    onDataLoaded(storiesResponse.stories);
                }
                loadFinished();
            }

            @Override
            public void failure(RetrofitError error) {
                loadFinished();
            }
        });
    }


 private void loadDesignerNewsRecent(final int page) {
        getDesignerNewsApi().getRecentStories(page, new Callback<StoriesResponse>() {
            @Override
            public void success(StoriesResponse storiesResponse, Response response) {
                if (storiesResponse != null
                        && sourceIsEnabled(SourceManager.SOURCE_DESIGNER_NEWS_RECENT)) {
                    setPage(storiesResponse.stories, page);
                    setDataSource(storiesResponse.stories,
                            SourceManager.SOURCE_DESIGNER_NEWS_RECENT);
                    onDataLoaded(storiesResponse.stories);
                }
                loadFinished();
            }

            @Override
            public void failure(RetrofitError error) {
                loadFinished();
            }
        });
    }

Thursday, January 21, 2016

Understanding Architecture of Nick Butcher's Plaid App - Part-2

FilterAdapter

Adapter for showing list of data sources in Drawer.




Note that there are only three items in the list that need authorization

1
2
3
4
5
6
7
8
9
private boolean isAuthorisedDribbbleSource(Source source) {

        return source.key.equals(SourceManager.SOURCE_DRIBBBLE_FOLLOWING)

                || source.key.equals(SourceManager.SOURCE_DRIBBBLE_USER_LIKES)

                || source.key.equals(SourceManager.SOURCE_DRIBBBLE_USER_SHOTS);

    }


Two important interfaces this class implements
  •   FilterAuthoriser

      The whole purpose of this authorizer callback is if you select any items in the drawer that
       require Dribble login , then this Callback would be invoked and HomeActivity will call the
       Dribble login activity 

1
2
3
 public interface FilterAuthoriser {
        void requestDribbbleAuthorisation(View sharedElement, Source forSource);
    }

 

 Constructor :


1
2
3
4
5
6
7
  public FilterAdapter(@NonNull Context context,
                         @NonNull List<Source> filters,
                         @NonNull FilterAuthoriser authoriser) {  <- This is an interface , so that the list items which                  this.context = context.getApplicationContext();   require this authorization , we can call this callback
          this.filters = filters;                                                     provided by initializer
          this.authoriser = authoriser;
          setHasStableIds(true);
    }
This is how its called from onCreate () in HomeActivity

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
   filtersAdapter = new FilterAdapter(this, SourceManager.getSources(this),
                new FilterAdapter.FilterAuthoriser() {
            @Override
            public void requestDribbbleAuthorisation(View sharedElemeent, Source forSource) {
                Intent login = new Intent(HomeActivity.this, DribbbleLogin.class);
                login.putExtra(FabDialogMorphSetup.EXTRA_SHARED_ELEMENT_START_COLOR,
                        ContextCompat.getColor(HomeActivity.this, R.color.background_dark));
                ActivityOptions options =
                        ActivityOptions.makeSceneTransitionAnimation(HomeActivity.this,
                                sharedElemeent, getString(R.string.transition_dribbble_login));
                startActivityForResult(login,
                        getAuthSourceRequestCode(forSource), options.toBundle());
            }
        });
  •  FiltersChangedListener

     Selection / Unselection of these items in the drawer are reported to HomeActivity using this
     listener.

 public interface FiltersChangedListener {
        void onFiltersChanged(Source changedFilter);
        void onFilterRemoved(Source removed);
    }

And helper method to set this listener

public void addFilterChangedListener(FiltersChangedListener listener) {
        if (listeners == null) {
            listeners = new ArrayList<>();
        }
        listeners.add(listener);
    }

Question is how is HomeActivity notified of these changes ,  

 


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 filtersAdapter.addFilterChangedListener(filtersChangedListener);

  private FilterAdapter.FiltersChangedListener filtersChangedListener =
            new FilterAdapter.FiltersChangedListener() {
        @Override
        public void onFiltersChanged(Source changedFilter) {
            if (!changedFilter.active) {
                adapter.removeDataSource(changedFilter.key);
            }
            checkEmptyState();
        }

        @Override
        public void onFilterRemoved(Source removed) {
            adapter.removeDataSource(removed.key);
            checkEmptyState();
        }
    };