From 17402765b6641daded84b64da494208154b98c02 Mon Sep 17 00:00:00 2001 From: esensar Date: Mon, 3 Oct 2016 23:44:53 +0200 Subject: [PATCH] Add live search for github users --- .idea/vcs.xml | 6 + app/build.gradle | 10 +- app/src/main/AndroidManifest.xml | 4 +- .../reactivegithubsample/MainActivity.java | 13 - .../models/GitHubRepo.java | 20 ++ .../models/GitHubSearchResponse.java | 21 ++ .../models/GitHubUser.java | 38 +++ .../network/GitHubApi.java | 27 +++ .../network/RestClient.java | 34 +++ .../views/GitHubUsersAdapter.java | 222 ++++++++++++++++++ .../views/MainActivity.java | 146 ++++++++++++ app/src/main/res/layout/activity_main.xml | 17 +- app/src/main/res/layout/list_item.xml | 59 +++++ app/src/main/res/layout/repo.xml | 5 + app/src/main/res/values/colors.xml | 4 +- 15 files changed, 605 insertions(+), 21 deletions(-) create mode 100644 .idea/vcs.xml delete mode 100644 app/src/main/java/com/ensarsarajcic/reactivegithubsample/MainActivity.java create mode 100644 app/src/main/java/com/ensarsarajcic/reactivegithubsample/models/GitHubRepo.java create mode 100644 app/src/main/java/com/ensarsarajcic/reactivegithubsample/models/GitHubSearchResponse.java create mode 100644 app/src/main/java/com/ensarsarajcic/reactivegithubsample/models/GitHubUser.java create mode 100644 app/src/main/java/com/ensarsarajcic/reactivegithubsample/network/GitHubApi.java create mode 100644 app/src/main/java/com/ensarsarajcic/reactivegithubsample/network/RestClient.java create mode 100644 app/src/main/java/com/ensarsarajcic/reactivegithubsample/views/GitHubUsersAdapter.java create mode 100644 app/src/main/java/com/ensarsarajcic/reactivegithubsample/views/MainActivity.java create mode 100644 app/src/main/res/layout/list_item.xml create mode 100644 app/src/main/res/layout/repo.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 6c66bb8..03e304f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,6 +24,14 @@ dependencies { androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) - compile 'com.android.support:appcompat-v7:24.0.0-beta1' + compile 'com.android.support:appcompat-v7:24.2.1' + compile 'com.android.support:recyclerview-v7:24.2.1' + compile 'com.google.code.gson:gson:2.4' + compile 'com.squareup.retrofit2:retrofit:2.1.0' + compile 'com.squareup.retrofit2:converter-gson:2.1.0' + compile 'com.jakewharton.rxbinding:rxbinding:0.4.0' testCompile 'junit:junit:4.12' + + compile 'io.reactivex:rxandroid:1.2.1' + compile 'io.reactivex:rxjava:1.2.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a8ef6a3..2145ebf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,13 +2,15 @@ + + - + diff --git a/app/src/main/java/com/ensarsarajcic/reactivegithubsample/MainActivity.java b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/MainActivity.java deleted file mode 100644 index 5a7913e..0000000 --- a/app/src/main/java/com/ensarsarajcic/reactivegithubsample/MainActivity.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.ensarsarajcic.reactivegithubsample; - -import android.support.v7.app.AppCompatActivity; -import android.os.Bundle; - -public class MainActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - } -} diff --git a/app/src/main/java/com/ensarsarajcic/reactivegithubsample/models/GitHubRepo.java b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/models/GitHubRepo.java new file mode 100644 index 0000000..2b6296e --- /dev/null +++ b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/models/GitHubRepo.java @@ -0,0 +1,20 @@ +package com.ensarsarajcic.reactivegithubsample.models; + +import com.google.gson.annotations.SerializedName; + +/** + * Created by ensar on 03/10/16. + */ +public class GitHubRepo { + public static final String TAG = GitHubRepo.class.getSimpleName(); + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/app/src/main/java/com/ensarsarajcic/reactivegithubsample/models/GitHubSearchResponse.java b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/models/GitHubSearchResponse.java new file mode 100644 index 0000000..1a19d03 --- /dev/null +++ b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/models/GitHubSearchResponse.java @@ -0,0 +1,21 @@ +package com.ensarsarajcic.reactivegithubsample.models; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +/** + * Created by ensar on 03/10/16. + */ +public class GitHubSearchResponse { + + private List items; + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/app/src/main/java/com/ensarsarajcic/reactivegithubsample/models/GitHubUser.java b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/models/GitHubUser.java new file mode 100644 index 0000000..d9d35e4 --- /dev/null +++ b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/models/GitHubUser.java @@ -0,0 +1,38 @@ +package com.ensarsarajcic.reactivegithubsample.models; + +import com.google.gson.annotations.SerializedName; + +/** + * Created by ensar on 03/10/16. + */ +public class GitHubUser { + public static final String TAG = GitHubUser.class.getSimpleName(); + + private String login; + private String html_url; + private String avatar_url; + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getHtml_url() { + return html_url; + } + + public void setHtml_url(String html_url) { + this.html_url = html_url; + } + + public String getAvatar_url() { + return avatar_url; + } + + public void setAvatar_url(String avatar_url) { + this.avatar_url = avatar_url; + } +} diff --git a/app/src/main/java/com/ensarsarajcic/reactivegithubsample/network/GitHubApi.java b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/network/GitHubApi.java new file mode 100644 index 0000000..58eef95 --- /dev/null +++ b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/network/GitHubApi.java @@ -0,0 +1,27 @@ +package com.ensarsarajcic.reactivegithubsample.network; + +import com.ensarsarajcic.reactivegithubsample.models.GitHubRepo; +import com.ensarsarajcic.reactivegithubsample.models.GitHubSearchResponse; +import com.ensarsarajcic.reactivegithubsample.models.GitHubUser; + +import java.util.List; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Path; +import retrofit2.http.Query; + +/** + * Created by ensar on 03/10/16. + */ +public interface GitHubApi { + + @GET("/users") + Call> getUsers(@Query("since") Integer since); + + @GET("/search/users") + Call searchForUsers(@Query("q") String query); + + @GET("/users/{user}/repos") + Call> getUserRepos(@Path("user") String user); +} diff --git a/app/src/main/java/com/ensarsarajcic/reactivegithubsample/network/RestClient.java b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/network/RestClient.java new file mode 100644 index 0000000..2b56725 --- /dev/null +++ b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/network/RestClient.java @@ -0,0 +1,34 @@ +package com.ensarsarajcic.reactivegithubsample.network; + +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +/** + * Created by ensar on 03/10/16. + */ +public class RestClient { + public static final String TAG = RestClient.class.getSimpleName(); + + private static GitHubApi gitHubApi; + private static Retrofit restAdapter = null; + + public static Retrofit getRestAdapter() { + if(restAdapter == null) { + Retrofit.Builder builder = new Retrofit.Builder() + .addConverterFactory(GsonConverterFactory.create()) + .baseUrl("https://api.github.com/"); + restAdapter = builder.build(); + } + return restAdapter; + } + + + public static GitHubApi getGitHubApi() { + if(gitHubApi == null) { + gitHubApi = getRestAdapter().create(GitHubApi.class); + } + return gitHubApi; + } + + +} diff --git a/app/src/main/java/com/ensarsarajcic/reactivegithubsample/views/GitHubUsersAdapter.java b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/views/GitHubUsersAdapter.java new file mode 100644 index 0000000..7dc9e30 --- /dev/null +++ b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/views/GitHubUsersAdapter.java @@ -0,0 +1,222 @@ +package com.ensarsarajcic.reactivegithubsample.views; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import com.ensarsarajcic.reactivegithubsample.R; +import com.ensarsarajcic.reactivegithubsample.models.GitHubRepo; +import com.ensarsarajcic.reactivegithubsample.models.GitHubUser; +import com.ensarsarajcic.reactivegithubsample.network.RestClient; +import com.jakewharton.rxbinding.view.RxView; +import com.jakewharton.rxbinding.widget.RxCompoundButton; +import com.jakewharton.rxbinding.widget.RxTextView; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import rx.Observable; +import rx.Subscriber; +import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Func1; +import rx.functions.Func2; +import rx.schedulers.Schedulers; +import rx.subscriptions.CompositeSubscription; + + +/** + * Created by ensar on 03/10/16. + */ +public class GitHubUsersAdapter extends RecyclerView.Adapter { + public static final String TAG = GitHubUsersAdapter.class.getSimpleName(); + + private List users; + private CompositeSubscription compositeSubscription; + + public GitHubUsersAdapter(List users) { + this.users = users; + compositeSubscription = new CompositeSubscription(); + } + + public void setItems(List users) { + this.users = users; + } + + @Override + public GitHubUserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View itemView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.list_item, parent, false); + + return new GitHubUserViewHolder(itemView); + } + + @Override + public void onBindViewHolder(final GitHubUserViewHolder holder, int position) { + GitHubUser gitHubUser = users.get(position); + holder.tvUserName.setText(gitHubUser.getLogin()); + holder.tvUserUrl.setText(gitHubUser.getHtml_url()); + + Observable fetchImageObservable = Observable.just(gitHubUser).startWith(new GitHubUser()) + .map(new Func1() { + @Override + public Bitmap call(GitHubUser gitHubUser) { + if(gitHubUser.getAvatar_url() == null) { + return BitmapFactory.decodeResource(holder.itemView.getContext().getResources(), R.mipmap.ic_launcher); + } + + try { + URL url = new URL(gitHubUser.getAvatar_url()); + return BitmapFactory.decodeStream(url.openConnection().getInputStream()); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + }) + .filter(new Func1() { + @Override + public Boolean call(Bitmap bitmap) { + return bitmap != null; + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + + Observable> fetchUserReposObservable = Observable.just(gitHubUser) + .map(new Func1>() { + @Override + public List call(GitHubUser gitHubUser) { + try { + return RestClient.getGitHubApi().getUserRepos(gitHubUser.getLogin()).execute().body(); + } catch (IOException e) { + e.printStackTrace(); + Log.e(TAG, "call: ", e); + return null; + } + } + }) + .filter(new Func1, Boolean>() { + @Override + public Boolean call(List gitHubRepos) { + return gitHubRepos != null; + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + +// Observable randomReposObservable = RxView.clicks(holder.tvRepos).subscribeOn(AndroidSchedulers.mainThread()); +// +// compositeSubscription.add(randomReposObservable.subscribe(new Subscriber() { +// @Override +// public void onCompleted() { +// Log.d(TAG, "onCompleted: "); +// } +// +// @Override +// public void onError(Throwable e) { +// Log.d(TAG, "onError: "); +// } +// +// @Override +// public void onNext(Void aVoid) { +// Log.d(TAG, "onNext: "); +// } +// })); +// +// Observable> gitHubReposObservable = Observable.combineLatest(randomReposObservable, fetchUserReposObservable, new Func2, List>() { +// @Override +// public List call(Void aVoid, List gitHubRepos) { +// return gitHubRepos; +// } +// }).subscribeOn(AndroidSchedulers.mainThread()); + + Observable> repoNamesObservable = fetchUserReposObservable.map(new Func1, List>() { + @Override + public List call(List gitHubRepos) { + List names = new ArrayList(); + for (int i = 0; i < 3; i++) { + if(gitHubRepos.isEmpty()) break; + int position = new Random().nextInt(gitHubRepos.size()); + names.add(gitHubRepos.get(position).getName()); + gitHubRepos.remove(position); + } + return names; + } + }); + + + + compositeSubscription.add(fetchImageObservable.subscribe(new Subscriber() { + @Override + public void onCompleted() { + } + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Bitmap bitmap) { + holder.ivUser.setImageBitmap(bitmap); + } + })); + + final ArrayAdapter stringArrayAdapter = new ArrayAdapter(holder.itemView.getContext(), R.layout.repo); + holder.lvRepos.setAdapter(stringArrayAdapter); + + compositeSubscription.add(repoNamesObservable.subscribe(new Subscriber>() { + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(List strings) { + stringArrayAdapter.clear(); + stringArrayAdapter.addAll(strings); + stringArrayAdapter.notifyDataSetChanged(); + } + })); + } + + @Override + public int getItemCount() { + return users.size(); + } + + public class GitHubUserViewHolder extends RecyclerView.ViewHolder { + private ImageView ivUser; + private TextView tvUserName; + private TextView tvUserUrl; + private ListView lvRepos; + + public GitHubUserViewHolder(View itemView) { + super(itemView); + ivUser = (ImageView) itemView.findViewById(R.id.ivUser); + tvUserName = (TextView) itemView.findViewById(R.id.tvUserName); + tvUserUrl = (TextView) itemView.findViewById(R.id.tvUserUrl); + lvRepos = (ListView) itemView.findViewById(R.id.lvRepos); + } + } + + public void clearSubscriptions() { + if(!compositeSubscription.isUnsubscribed()) { + compositeSubscription.unsubscribe(); + } + } +} diff --git a/app/src/main/java/com/ensarsarajcic/reactivegithubsample/views/MainActivity.java b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/views/MainActivity.java new file mode 100644 index 0000000..2f12886 --- /dev/null +++ b/app/src/main/java/com/ensarsarajcic/reactivegithubsample/views/MainActivity.java @@ -0,0 +1,146 @@ +package com.ensarsarajcic.reactivegithubsample.views; + +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.support.v7.widget.DefaultItemAnimator; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.widget.EditText; + +import com.ensarsarajcic.reactivegithubsample.R; +import com.ensarsarajcic.reactivegithubsample.models.GitHubSearchResponse; +import com.ensarsarajcic.reactivegithubsample.models.GitHubUser; +import com.ensarsarajcic.reactivegithubsample.network.RestClient; +import com.jakewharton.rxbinding.widget.RxTextView; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import rx.Observable; +import rx.Subscriber; +import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Func1; +import rx.schedulers.Schedulers; +import rx.subscriptions.CompositeSubscription; + +public class MainActivity extends AppCompatActivity { + private static final String TAG = MainActivity.class.getSimpleName(); + + EditText etSearch; + RecyclerView rvUsers; + + CompositeSubscription compositeSubscription; + + Observable textChangeStream; + Observable> gitHubSearchResponseStream; + Observable> gitHubUsersResponseStream; + Observable> gitHubAllResponsesStream; + + GitHubUsersAdapter adapter; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + etSearch = (EditText) findViewById(R.id.etSearch); + rvUsers = (RecyclerView) findViewById(R.id.rvUsers); + + adapter = new GitHubUsersAdapter(new ArrayList()); + rvUsers.setAdapter(adapter); + RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getApplicationContext()); + rvUsers.setLayoutManager(layoutManager); + rvUsers.setItemAnimator(new DefaultItemAnimator()); + + compositeSubscription = new CompositeSubscription(); + textChangeStream = RxTextView.textChanges(etSearch). + debounce(1, TimeUnit.SECONDS).subscribeOn(AndroidSchedulers.mainThread()); + + gitHubSearchResponseStream = textChangeStream.filter(new Func1() { + @Override + public Boolean call(CharSequence charSequence) { + return !TextUtils.isEmpty(charSequence); + } + }) + .map(new Func1() { + @Override + public GitHubSearchResponse call(CharSequence charSequence) { + try { + return RestClient.getGitHubApi().searchForUsers(charSequence.toString()).execute().body(); + } catch (IOException ioException) { + ioException.printStackTrace(); + return null; + } + } + }) + .filter(new Func1() { + @Override + public Boolean call(GitHubSearchResponse gitHubSearchResponse) { + return gitHubSearchResponse != null; + } + }) + .map(new Func1>() { + @Override + public List call(GitHubSearchResponse gitHubSearchResponse) { + return gitHubSearchResponse.getItems(); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + + gitHubUsersResponseStream = textChangeStream. + filter(new Func1() { + @Override + public Boolean call(CharSequence charSequence) { + return TextUtils.isEmpty(charSequence); + } + }) + .map(new Func1>() { + @Override + public List call(CharSequence charSequence) { + try { + return RestClient.getGitHubApi().getUsers(new Random().nextInt(1000)).execute().body(); + } catch (IOException ioException) { + ioException.printStackTrace(); + return new ArrayList(); + } + } + }) + .subscribeOn(Schedulers.io()). + observeOn(AndroidSchedulers.mainThread()); + + gitHubAllResponsesStream = Observable.merge(gitHubSearchResponseStream, gitHubUsersResponseStream); + + compositeSubscription.add(gitHubAllResponsesStream.subscribe(new Subscriber>() { + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onNext(List gitHubUsers) { + adapter.setItems(gitHubUsers); + adapter.notifyDataSetChanged(); + } + })); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + adapter.clearSubscriptions(); + if(!compositeSubscription.isUnsubscribed()) { + compositeSubscription.unsubscribe(); + } + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 624b2fd..01c8e51 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -8,10 +8,19 @@ android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" - tools:context="com.ensarsarajcic.reactivegithubsample.MainActivity"> + tools:context="com.ensarsarajcic.reactivegithubsample.views.MainActivity"> - + android:text="Search" /> + + diff --git a/app/src/main/res/layout/list_item.xml b/app/src/main/res/layout/list_item.xml new file mode 100644 index 0000000..4116488 --- /dev/null +++ b/app/src/main/res/layout/list_item.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/repo.xml b/app/src/main/res/layout/repo.xml new file mode 100644 index 0000000..69fb6b5 --- /dev/null +++ b/app/src/main/res/layout/repo.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 3ab3e9c..2e7d32b 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,6 +1,6 @@ - #3F51B5 + #123DE1 #303F9F - #FF4081 + #22AACC