Share
Embed
REST API's with Retrofit and Android
Retrofit, Android, Gradle, Gson, AsyncTask, ViewHolder pattern
Jo Somers
April 04, 2014
Transcript
112 slides
-
REST API’s with ANDROID and RETROFIT Email : [email protected] Twitter/Github
: josomers LinkedIn : linkedin.com/in/josomers -
24 years old Graduated in 2011 Applied Computer Sciences: Multimedia
Strong interest in front-end development @josomers -
Every day more than 1 million new Android devices are
activated worldwide. -
Android users download more than 1.5 billion apps and games
from Google Play each month and growing... -
Android Studio (or Eclipse) d.android.com Registration for only 25$ Find
all the design guidelines -
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 -
<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 -
@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 -
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); } -
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); } -
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 -
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 -
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(); } }; -
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 -
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 !? -
@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 -
<?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 -
@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 -
@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? -
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 -
! @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 -
ListView QuestionAdapter EmptyView List of questions Generate view for each
question Recycle views QuestionsActivity Layout -
@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 -
@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": []! } -
@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 -
private void initQuestion() { final GsonHelper<Question> gsonParser = new GsonHelper<Question>(new
TypeToken<Question>() { }.getType()); ! question = gsonParser.fromJson(getIntent().getStringExtra("question")); } QuestionsDetailActivity.java QuestionsDetailActivity -
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; } -
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> !!! -
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! -
Async Task AsyncTask<Void, Void, Void> { ... } 1) onPreExecute()
2) doInBackground (Params ...) 3) onProgressUpdate(Progress ...) 4) onPostExecute(Result) -
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");! }! }! -
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 -
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 -
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()); } -
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()); } -
{! "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 -
Controller ‘Fetch Data’ From Server & get ‘Response’ ‘Parse Data’
to List<Question> GET a list of questions -
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 -
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); ! } -
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(); } } -
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()); } -
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 -
Doing a POST request Create a new question on the
device. Send it to the server. -
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 -
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); ! } -
@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(); } } -
{! "question": "Can examinations be taken in English?",! "created": "2014-03-27T22:54:26.052Z",!
"modified": "2014-03-27T22:54:26.052Z",! "id": "9d8df237cfcd689a",! "answers": []! } -
http://developer.android.com/tools/testing/testing_android.html Test Droid : http://testdroid.com/ Robolectric : http://pivotal.github.io/robolectric/ Fest Android
: http://square.github.io/fest-android/ ... -
@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> -
We made this demo for minSdkVersion=”14” and targetSdkVersion=”19” = Ice
Cream Sandwich/Jelly Bean/KitKat -
- 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" /> -
Android 4.2 : Tap 7 times on the build number
to enable developer mode -
Follow Android developers on G+ Read articles about Android Resources
every Android developer must know http://cl.ly/1Z3z0G440H3E -
! - Drawables : hdpi/xxhdpi/xdpi/... - Styles for Actionbar, widgets…
- Holo styles & colors - App Launcher Icons - Menu Icons - Generic icons ! - Device Frame Generator -
Check out the project: - SourceTree - Terminal - ...
https://bitbucket.org/josomers/kdg-inf-demo-android