Upgrade to Pro — share decks privately, control downloads, hide ads and more …

REST API's with Retrofit and Android

Jo Somers
April 04, 2014

REST API's with Retrofit and Android

Retrofit, Android, Gradle, Gson, AsyncTask, ViewHolder pattern

Jo Somers

April 04, 2014
Tweet

More Decks by Jo Somers

Other Decks in Programming

Transcript

  1. 24 years old Graduated in 2011 Applied Computer Sciences: Multimedia

    Strong interest in front-end development @josomers
  2. Android users download more than
 1.5 billion apps and games


    from Google Play each month and growing...
  3. 1. Easy Android project setup 2. Android application structure 3.

    Client - Server communication 4. Tips & Tricks Get a list of Questions + (Get a list of Answers by ‘questionId’) Add a new Question/Answer
  4. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:descendantFocusability="beforeDescendants" android:focusableInTouchMode="true" tools:context="be.kdgdemo.activities.QuestionsActivity">

    ! <ListView android:id="@+id/listview" android:layout_width="match_parent" android:layout_height="match_parent" android:cacheColorHint="@android:color/transparent" android:footerDividersEnabled="false" android:headerDividersEnabled="false" android:listSelector="@drawable/question_item_bg" android:overScrollMode="never" android:scrollbarStyle="outsideOverlay" /> ! <TextView android:id="@+id/empty" android:layout_width="match_parent" android:layout_height="0dp" android:layout_gravity="center" android:layout_weight="1" android:gravity="center" android:text="@string/tQuestionsEmpty" /> ! </LinearLayout> activity_questions.xml
  5. @InjectView(R.id.listview) ListView listView; ! @InjectView(R.id.empty) View emptyView; ! private QuestionAdapter

    adapter; ! @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ! setContentView(R.layout.activity_questions); ! ButterKnife.inject(this); ! adapter = new QuestionAdapter(this); ! listView.setAdapter(adapter); listView.setEmptyView(emptyView); listView.setOnItemClickListener(this); listView.setOnItemLongClickListener(this); } QuestionsActivity.java
  6. QuestionsActivity.java @InjectView(R.id.listview) ListView listView; ! @InjectView(R.id.empty) View emptyView; ! private

    QuestionAdapter adapter; ! @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ! setContentView(R.layout.activity_questions); ! ButterKnife.inject(this); ! adapter = new QuestionAdapter(this); ! listView.setAdapter(adapter); listView.setEmptyView(emptyView); listView.setOnItemClickListener(this); listView.setOnItemLongClickListener(this); }
  7. QuestionsActivity.java Adapter @InjectView(R.id.listview) ListView listView; ! @InjectView(R.id.empty) View emptyView; !

    private QuestionAdapter adapter; ! @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ! setContentView(R.layout.activity_questions); ! ButterKnife.inject(this); ! adapter = new QuestionAdapter(this); ! listView.setAdapter(adapter); listView.setEmptyView(emptyView); listView.setOnItemClickListener(this); listView.setOnItemLongClickListener(this); }
  8. private final static Ordering<Question> QUESTION_ORDERING = new Ordering<Question>() { @Override

    public int compare(Question left, Question right) { return right.getModified().compareTo(left.getModified()); } }; ! private final Context context; ! private List<Question> questions; ! public QuestionAdapter(Context context) { this.context = context; this.questions = newArrayList(); } ! public void setQuestions(List<Question> questions) { this.questions = QUESTION_ORDERING.sortedCopy(questions); notifyDataSetChanged(); } ! public void remove(int position) { this.questions.remove(position); notifyDataSetChanged(); } Questiondapter.java
  9. private final static Ordering<Question> QUESTION_ORDERING = new Ordering<Question>() { @Override

    public int compare(Question left, Question right) { return right.getModified().compareTo(left.getModified()); } }; ! private final Context context; ! private List<Question> questions; ! public QuestionAdapter(Context context) { this.context = context; this.questions = newArrayList(); } ! public void setQuestions(List<Question> questions) { this.questions = QUESTION_ORDERING.sortedCopy(questions); notifyDataSetChanged(); } ! public void remove(int position) { this.questions.remove(position); notifyDataSetChanged(); } Questiondapter.java
  10. new Ordering<Match>() { @Override public int compare(Match left, Match right)

    { final String lhsDivisionName = left.getDivision() != null ? left.getDivision().getName() : null; final String rhsDivisionName = right.getDivision() != null ? right.getDivision().getName() : null; ! return start() .compare(lhsDivisionName, rhsDivisionName, natural().nullsFirst()) .compare(left.getMatchDay(), right.getMatchDay(), natural().nullsFirst()) .compare(left.getStartTime(), right.getStartTime(), natural().nullsFirst()) .result(); } };
  11. Answer public class Question { ! private String id; private

    String question; private DateTime modified; private List<Answer> answers; ! /** * Mandatory empty constructor */ public Question() { } ! public String getQuestion() { return question; } ! public void setQuestion(String question) { this.question = question; } ! public List<Answer> getAnswers() { return answers; } ! public DateTime getModified() { return modified; } ! public String getId() { return id; } ! } public class Answer { ! private String questionId; private String answer; private DateTime modified; ! /** * Mandatory empty constructor */ public Answer() { } ! public String getAnswer() { return answer; } ! public DateTime getModified() { return modified; } ! public void setAnswer(String answer) { this.answer = answer; } ! public void setQuestionId(String questionId) { this.questionId = questionId; } } Question Question.java Answer.java
  12. Answer public class Question { ! private String id; private

    String question; private DateTime modified; private List<Answer> answers; ! /** * Mandatory empty constructor */ public Question() { } ! public String getQuestion() { return question; } ! public void setQuestion(String question) { this.question = question; } ! public List<Answer> getAnswers() { return answers; } ! public DateTime getModified() { return modified; } ! public String getId() { return id; } ! } public class Answer { ! private String questionId; private String answer; private DateTime modified; ! /** * Mandatory empty constructor */ public Answer() { } ! public String getAnswer() { return answer; } ! public DateTime getModified() { return modified; } ! public void setAnswer(String answer) { this.answer = answer; } ! public void setQuestionId(String questionId) { this.questionId = questionId; } } Question Question.java Answer.java !?
  13. @Override public View getView(int position, View convertView, ViewGroup parent) {

    final Question question = getItem(position); ! ViewHolder viewHolder; if (convertView != null) { viewHolder = (ViewHolder) convertView.getTag(); } else { convertView = from(context).inflate(R.layout.question_item, parent, false); viewHolder = new ViewHolder(convertView); convertView.setTag(viewHolder); } ! viewHolder.modifiedTextView.setText(question.getModified().toString("dd-MM-yyyy hh:mm")); viewHolder.questionTextView.setText(question.getQuestion()); viewHolder.numberOfAnswersTextView.setText("" + question.getAnswers().size()); ! return convertView; } QuestionAdapter.java
  14. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin"

    android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin"> ! <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> ! <TextView android:id="@+id/modified" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="5dp" android:layout_weight="1" android:ellipsize="end" android:singleLine="true" android:textSize="15sp" android:textStyle="italic" tools:text="23-06-2014" /> ! <TextView android:id="@+id/numberOfAnswers" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/activity_horizontal_margin" android:ellipsize="end" android:singleLine="true" android:textSize="15sp" tools:text="1" /> ! </LinearLayout> ! <TextView android:id="@+id/question" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="15sp" tools:text="Lorem Ipsum has been the industry's standard." /> ! </LinearLayout> question_item.xml
  15. @Override public View getView(int position, View convertView, ViewGroup parent) {

    final Question question = getItem(position); ! ViewHolder viewHolder; if (convertView != null) { viewHolder = (ViewHolder) convertView.getTag(); } else { convertView = from(context).inflate(R.layout.question_item, parent, false); viewHolder = new ViewHolder(convertView); convertView.setTag(viewHolder); } ! viewHolder.modifiedTextView.setText(question.getModified().toString("dd-MM-yyyy hh:mm")); viewHolder.questionTextView.setText(question.getQuestion()); viewHolder.numberOfAnswersTextView.setText("" + question.getAnswers().size()); ! return convertView; } QuestionAdapter.java Bind the data to the views
  16. @Override public View getView(int position, View convertView, ViewGroup parent) {

    final Question question = getItem(position); ! ViewHolder viewHolder; if (convertView != null) { viewHolder = (ViewHolder) convertView.getTag(); } else { convertView = from(context).inflate(R.layout.question_item, parent, false); viewHolder = new ViewHolder(convertView); convertView.setTag(viewHolder); } ! viewHolder.modifiedTextView.setText(question.getModified().toString("dd-MM-yyyy hh:mm")); viewHolder.questionTextView.setText(question.getQuestion()); viewHolder.numberOfAnswersTextView.setText("" + question.getAnswers().size()); ! return convertView; } QuestionAdapter.java ConvertView?
  17. static class ViewHolder { @InjectView(R.id.modified) TextView modifiedTextView; ! @InjectView(R.id.question) TextView

    questionTextView; ! @InjectView(R.id.numberOfAnswers) TextView numberOfAnswersTextView; ! public ViewHolder(View view) { ButterKnife.inject(this, view); } } QuestionAdapter.java
  18. ! @Override public int getItemViewType(int position) { return position%2; }

    ! @Override public int getViewTypeCount() { return 2; } ViewType ? • Altering rows • Headers • Different content: Questions & Answers in 1 adapter with different views
  19. ListView QuestionAdapter EmptyView List of questions Generate view for each

    question Recycle views QuestionsActivity Layout
  20. @Override public void onItemClick( final AdapterView<?> parent, final View view,

    final int position, final long id) { ! final GsonHelper<Question> gsonMarshaller = new GsonHelper<Question>(new TypeToken<Question>() { }.getType()); ! final Intent detailIntent = new Intent(this, QuestionsDetailActivity.class); detailIntent.putExtra("question", gsonMarshaller.toJson(adapter.getItem(position))); startActivity(detailIntent); } QuestionsActivity QuestionsDetailActivity
  21. @Override public void onItemClick( final AdapterView<?> parent, final View view,

    final int position, final long id) { ! final GsonHelper<Question> gsonMarshaller = new GsonHelper<Question>(new TypeToken<Question>() { }.getType()); ! final Intent detailIntent = new Intent(this, QuestionsDetailActivity.class); detailIntent.putExtra("question", gsonMarshaller.toJson(adapter.getItem(position))); startActivity(detailIntent); } QuestionsActivity QuestionsDetailActivity {! "question": "How does a number porting proceed?",! "created": "2014-04-02T08:37:43.766Z",! "modified": "2014-04-02T08:37:43.766Z",! "id": "8a558826d4a71bd2",! "answers": []! }
  22. @Override public void onItemClick( final AdapterView<?> parent, final View view,

    final int position, final long id) { ! final GsonHelper<Question> gsonMarshaller = new GsonHelper<Question>(new TypeToken<Question>() { }.getType()); ! final Intent detailIntent = new Intent(this, QuestionsDetailActivity.class); detailIntent.putExtra("question", gsonMarshaller.toJson(adapter.getItem(position))); startActivity(detailIntent); } QuestionsActivity QuestionsDetailActivity
  23. private void initQuestion() { final GsonHelper<Question> gsonParser = new GsonHelper<Question>(new

    TypeToken<Question>() { }.getType()); ! question = gsonParser.fromJson(getIntent().getStringExtra("question")); } QuestionsDetailActivity.java QuestionsDetailActivity
  24. QuestionsDetailActivity @Override public boolean onItemLongClick( final AdapterView<?> parent, final View

    view, final int position, final long id) { ! final Question question = adapter.getItem(position); ! final AlertDialogFragment.Callback questionCallback = new AlertDialogFragment.Callback() { @Override public void onPositiveButtonClicked() { deleteQuestion(position, question); } ! @Override public void onCancel() { // just dismiss, to nothing. } }; ! AlertDialogUtil.showDialog( getFragmentManager(), TAG, questionCallback, getString(R.string.tQuestionDeleteTitle), getString(R.string.tQuestionDeleteMessage).replace("{question}", "'" + question.getQuestion() + "'"), getString(R.string.tQuestionDeletePositive), getString(R.string.tQuestionDeleteNegative)); ! return true; }
  25. Manifest (1/2) <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="be.kdgdemo"> ! <uses-permission

    android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> ! <application android:name="be.kdgdemo.application.KdgDemoApplication" android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/tAppName" android:theme="@style/KdgTheme"> <activity android:name="be.kdgdemo.activities.QuestionsActivity" android:label="@string/tAppName"> <intent-filter> <action android:name="android.intent.action.MAIN" /> ! <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="be.kdgdemo.activities.QuestionsDetailActivity" /> <activity android:name="be.kdgdemo.activities.NewQuestionActivity" /> </application> ! </manifest> !!!
  26. Manifest (2/2) <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="be.kdgdemo"> ! <uses-permission

    android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> ! <application android:name="be.kdgdemo.application.KdgDemoApplication" android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/tAppName" android:theme="@style/KdgTheme"> <activity android:name="be.kdgdemo.activities.QuestionsActivity" android:label="@string/tAppName"> <intent-filter> <action android:name="android.intent.action.MAIN" /> ! <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="be.kdgdemo.activities.QuestionsDetailActivity" /> <activity android:name="be.kdgdemo.activities.NewQuestionActivity" /> </application> ! </manifest> Activities!
  27. Async Task AsyncTask<Void, Void, Void> { ... } 1) onPreExecute()

    2) doInBackground (Params ...) 3) onProgressUpdate(Progress ...) 4) onPostExecute(Result)
  28. Async Task new DownloadFilesTask().execute(url1, url2, url3);! private class DownloadFilesTask extends

    AsyncTask<URL, Integer, Long> {! protected Long doInBackground(URL... urls) {! int count = urls.length;! long totalSize = 0;! for (int i = 0; i < count; i++) {! totalSize += Downloader.downloadFile(urls[i]);! publishProgress((int) ((i / (float) count) * 100));! // Escape early if cancel() is called! if (isCancelled()) break;! }! return totalSize;! }! ! protected void onProgressUpdate(Integer... progress) {! setProgressPercent(progress[0]);! }! ! protected void onPostExecute(Long result) {! showDialog("Downloaded " + result + " bytes");! }! }!
  29. public class Question { ! private String id; private String

    question; private DateTime modified; private List<Answer> answers; ! /** * Mandatory empty constructor */ public Question() { } ! public String getQuestion() { return question; } ! public void setQuestion(String question) { this.question = question; } ! public List<Answer> getAnswers() { return answers; } ! public DateTime getModified() { return modified; } ! public String getId() { return id; } ! } Answer public class Answer { ! private String questionId; private String answer; private DateTime modified; ! /** * Mandatory empty constructor */ public Answer() { } ! public String getAnswer() { return answer; } ! public DateTime getModified() { return modified; } ! public void setAnswer(String answer) { this.answer = answer; } ! public void setQuestionId(String questionId) { this.questionId = questionId; } } Question Question.java Answer.java
  30. Client - Server Communication Connecting to a REST API POST

    GET a list of questions a list of answers by question a new question/answer QuestionsActivity DELETE a question
  31. QuestionsActivity @Override protected void onResume() { super.onResume(); refreshQuestions(); } QuestionsActivity.java

    private void refreshQuestions() { getService().getQuestions(this); } ! @Override public void success( final List<Question> questions, final Response response) { ! adapter.setQuestions(questions); } ! @Override public void failure(final RetrofitError error) { getVisualisation().showError(getString(R.string.tFailure)); ! Log.e(TAG, error.toString()); }
  32. QuestionsActivity @Override protected void onResume() { super.onResume(); refreshQuestions(); } QuestionsActivity.java

    private void refreshQuestions() { getService().getQuestions(this); } ! @Override public void success( final List<Question> questions, final Response response) { ! adapter.setQuestions(questions); } ! @Override public void failure(final RetrofitError error) { getVisualisation().showError(getString(R.string.tFailure)); ! Log.e(TAG, error.toString()); }
  33. {! "answer": "I don't know ;-)",! "created": "2014-04-02T06:30:42.497Z",! "modified": "2014-04-02T06:30:42.497Z",!

    "id": "dc86f5d2a7b1187f"! }! ]! },! {! "question": "Where can I learn Dutch or follow other lan "created": "2014-03-27T22:54:40.733Z",! "modified": "2014-03-27T22:54:40.733Z",! "id": "316333702e53187d",! "answers": [! {! "answer": "Lorum Ipsum is a simply dummy text of the pri dummy text every sinds the 1500s, when an unknown printe "created": "2014-04-01T22:12:28.357Z",! "modified": "2014-04-01T22:12:28.357Z",! "id": "8c5a3c674504c8a7"! },! {! "answer": "Yes! The first answer on this question. Thx!" "created": "2014-04-01T22:12:57.731Z",! "modified": "2014-04-01T22:12:57.731Z",! "id": "bcf257da749af86e"! }! ]! },! {! "question": "How does a number porting proceed?",! "created": "2014-04-02T08:37:43.766Z",! "modified": "2014-04-02T08:37:43.766Z",! "id": "8a558826d4a71bd2",! "answers": [! {! "answer": "When you port your number, it means that when http://kdg-inf-demo.herokuapp.com/questions
  34. Application w/ RestAdapter Builder Add parser —>Gson (or Jackson) —>

    parse string into object —> parse object to string GET a list of questions Define Retrofit interface for RestAdapter
  35. public interface KdgService { ! @GET("/questions") void getQuestions(Callback<List<Question>> callback); !

    @POST("/questions") void submitQuestion(@Body Question question, Callback<Question> created); ! @DELETE("/questions/{id}") void deleteQuestion(@Path("id") String questionId, Callback<Question> callback); ! @GET("/questions/{questionId}") void getQuestion(@Path("questionId") String questionId, Callback<Question> callback); ! @POST("/answers") void submitAnswer(@Body Answer answer, Callback<Answer> created); ! }
  36. And it all started with… HttpURLConnection! HttpURLConnection —> BufferedInputStream —>

    Read as String —> Parse string w/ Gson to desired Object http://developer.android.com/reference/java/net/HttpURLConnection.html @Override public String get(ResponseHandler responseHandler, String location) throws IOException { String fullUrl = mBaseURL + location; ! Log.v(TAG, "get data from: " + fullUrl); ! URL url = new URL(fullUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(mConnectTimeout); connection.setReadTimeout(mReadTimeout); try { InputStream stream = connection.getInputStream(); return responseHandler.handleResponse(new BufferedInputStream(stream)); } finally { connection.disconnect(); } }
  37. QuestionsActivity @Override protected void onResume() { super.onResume(); refreshQuestions(); } QuestionsActivity.java

    private void refreshQuestions() { getService().getQuestions(this); } ! @Override public void success( final List<Question> questions, final Response response) { ! adapter.setQuestions(questions); } ! @Override public void failure(final RetrofitError error) { getVisualisation().showError(getString(R.string.tFailure)); ! Log.e(TAG, error.toString()); }
  38. private final Context context; ! private List<Question> questions; ! public

    QuestionAdapter(Context context) { this.context = context; this.questions = newArrayList(); } ! public void setQuestions(List<Question> questions) { this.questions = QUESTION_ORDERING.sortedCopy(questions); notifyDataSetChanged(); } QuestionAdapter.java
  39. Doing a POST request Create a new question on the

    device. Send it to the server.
  40. private void submitQuestion() { questionInputEditText.setEnabled(false); submitQuestionButton.setEnabled(false); ! final String questionText

    = questionInputEditText != null && questionInputEditText.getText() != null ? questionInputEditText.getText().toString() : ""; ! final Question question = new Question(); question.setQuestion(questionText); ! getService().submitQuestion(question, this); } NewQuestionActivity.java
  41. public interface KdgService { ! @GET("/questions") void getQuestions(Callback<List<Question>> callback); !

    @POST("/questions") void submitQuestion(@Body Question question, Callback<Question> created); ! @DELETE("/questions/{id}") void deleteQuestion(@Path("id") String questionId, Callback<Question> callback); ! @GET("/questions/{questionId}") void getQuestion(@Path("questionId") String questionId, Callback<Question> callback); ! @POST("/answers") void submitAnswer(@Body Answer answer, Callback<Answer> created); ! }
  42. @Override public String post(ResponseHandler responseHandler, String location, String body) throws

    IOException { String fullUrl = mBaseURL + location; Log.v(TAG, "post data to: " + fullUrl); URL url = new URL(fullUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(mConnectTimeout); connection.setReadTimeout(mReadTimeout); connection.setDoOutput(true); connection.setDoInput(true); connection.setUseCaches(false); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("charset", "utf-8"); ! try { DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream()); outputStream.writeBytes(body); outputStream.flush(); outputStream.close(); ! return responseHandler.handleResponse(new BufferedInputStream(connection.getInputStream())); } finally { connection.disconnect(); } }
  43. {! "question": "Can examinations be taken in English?",! "created": "2014-03-27T22:54:26.052Z",!

    "modified": "2014-03-27T22:54:26.052Z",! "id": "9d8df237cfcd689a",! "answers": []! }
  44. @Override public void onStart() { super.onStart(); EasyTracker.getInstance().activityStart(this); } ! @Override

    public void onStop() { super.onStop(); EasyTracker.getInstance().activityStop(this); } <?xml version="1.0" encoding="utf-8"?> <resources> ! <string name="ga_trackingId">UA-39953761-1</string> ! <bool name="ga_autoActivityTracking">true</bool> <bool name="ga_reportUncaughtExceptions">true</bool> ! </resources>
  45. - Android Support Library (rev. 12)
 > Fragments / viewpager

    / searchview / cache
 - Open source libraries
 > Actionbar sherlock (by Jake Warthon)
 > AppCompat > Nineoldandroids
 - Define minSdkVersion <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="18" />
  46. Follow Android developers on G+ Read articles about Android Resources

    every Android developer
 must know http://cl.ly/1Z3z0G440H3E
  47. ! - Drawables : hdpi/xxhdpi/xdpi/... - Styles for Actionbar, widgets…

    - Holo styles & colors - App Launcher Icons - Menu Icons - Generic icons ! - Device Frame Generator
  48. Check out the project: - SourceTree - Terminal - ...

    https://bitbucket.org/josomers/kdg-inf-demo-android