Add live search for github users

master
esensar 2016-10-03 23:44:53 +02:00
parent 44de076a70
commit 17402765b6
15 changed files with 605 additions and 21 deletions

6
.idea/vcs.xml 100644
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -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'
}

View File

@ -2,13 +2,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ensarsarajcic.reactivegithubsample">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<activity android:name=".views.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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<GitHubUser> items;
public List<GitHubUser> getItems() {
return items;
}
public void setItems(List<GitHubUser> items) {
this.items = items;
}
}

View File

@ -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;
}
}

View File

@ -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<List<GitHubUser>> getUsers(@Query("since") Integer since);
@GET("/search/users")
Call<GitHubSearchResponse> searchForUsers(@Query("q") String query);
@GET("/users/{user}/repos")
Call<List<GitHubRepo>> getUserRepos(@Path("user") String user);
}

View File

@ -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;
}
}

View File

@ -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<GitHubUsersAdapter.GitHubUserViewHolder> {
public static final String TAG = GitHubUsersAdapter.class.getSimpleName();
private List<GitHubUser> users;
private CompositeSubscription compositeSubscription;
public GitHubUsersAdapter(List<GitHubUser> users) {
this.users = users;
compositeSubscription = new CompositeSubscription();
}
public void setItems(List<GitHubUser> 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<Bitmap> fetchImageObservable = Observable.just(gitHubUser).startWith(new GitHubUser())
.map(new Func1<GitHubUser, Bitmap>() {
@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<Bitmap, Boolean>() {
@Override
public Boolean call(Bitmap bitmap) {
return bitmap != null;
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
Observable<List<GitHubRepo>> fetchUserReposObservable = Observable.just(gitHubUser)
.map(new Func1<GitHubUser, List<GitHubRepo>>() {
@Override
public List<GitHubRepo> 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<List<GitHubRepo>, Boolean>() {
@Override
public Boolean call(List<GitHubRepo> gitHubRepos) {
return gitHubRepos != null;
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
// Observable<Void> randomReposObservable = RxView.clicks(holder.tvRepos).subscribeOn(AndroidSchedulers.mainThread());
//
// compositeSubscription.add(randomReposObservable.subscribe(new Subscriber<Void>() {
// @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<List<GitHubRepo>> gitHubReposObservable = Observable.combineLatest(randomReposObservable, fetchUserReposObservable, new Func2<Void, List<GitHubRepo>, List<GitHubRepo>>() {
// @Override
// public List<GitHubRepo> call(Void aVoid, List<GitHubRepo> gitHubRepos) {
// return gitHubRepos;
// }
// }).subscribeOn(AndroidSchedulers.mainThread());
Observable<List<String>> repoNamesObservable = fetchUserReposObservable.map(new Func1<List<GitHubRepo>, List<String>>() {
@Override
public List<String> call(List<GitHubRepo> gitHubRepos) {
List<String> names = new ArrayList<String>();
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<Bitmap>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(Bitmap bitmap) {
holder.ivUser.setImageBitmap(bitmap);
}
}));
final ArrayAdapter<String> stringArrayAdapter = new ArrayAdapter<String>(holder.itemView.getContext(), R.layout.repo);
holder.lvRepos.setAdapter(stringArrayAdapter);
compositeSubscription.add(repoNamesObservable.subscribe(new Subscriber<List<String>>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(List<String> 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();
}
}
}

View File

@ -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<CharSequence> textChangeStream;
Observable<List<GitHubUser>> gitHubSearchResponseStream;
Observable<List<GitHubUser>> gitHubUsersResponseStream;
Observable<List<GitHubUser>> 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<GitHubUser>());
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<CharSequence, Boolean>() {
@Override
public Boolean call(CharSequence charSequence) {
return !TextUtils.isEmpty(charSequence);
}
})
.map(new Func1<CharSequence, GitHubSearchResponse>() {
@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<GitHubSearchResponse, Boolean>() {
@Override
public Boolean call(GitHubSearchResponse gitHubSearchResponse) {
return gitHubSearchResponse != null;
}
})
.map(new Func1<GitHubSearchResponse, List<GitHubUser>>() {
@Override
public List<GitHubUser> call(GitHubSearchResponse gitHubSearchResponse) {
return gitHubSearchResponse.getItems();
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
gitHubUsersResponseStream = textChangeStream.
filter(new Func1<CharSequence, Boolean>() {
@Override
public Boolean call(CharSequence charSequence) {
return TextUtils.isEmpty(charSequence);
}
})
.map(new Func1<CharSequence, List<GitHubUser>>() {
@Override
public List<GitHubUser> call(CharSequence charSequence) {
try {
return RestClient.getGitHubApi().getUsers(new Random().nextInt(1000)).execute().body();
} catch (IOException ioException) {
ioException.printStackTrace();
return new ArrayList<GitHubUser>();
}
}
})
.subscribeOn(Schedulers.io()).
observeOn(AndroidSchedulers.mainThread());
gitHubAllResponsesStream = Observable.merge(gitHubSearchResponseStream, gitHubUsersResponseStream);
compositeSubscription.add(gitHubAllResponsesStream.subscribe(new Subscriber<List<GitHubUser>>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onNext(List<GitHubUser> gitHubUsers) {
adapter.setItems(gitHubUsers);
adapter.notifyDataSetChanged();
}
}));
}
@Override
protected void onDestroy() {
super.onDestroy();
adapter.clearSubscriptions();
if(!compositeSubscription.isUnsubscribed()) {
compositeSubscription.unsubscribe();
}
}
}

View File

@ -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">
<TextView
android:layout_width="wrap_content"
<EditText
android:id="@+id/etSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World!" />
android:text="Search" />
<android.support.v7.widget.RecyclerView
android:id="@+id/rvUsers"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/etSearch"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"/>
</RelativeLayout>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="200dp">
<TextView
android:layout_width="200dp"
android:layout_height="wrap_content"
android:id="@+id/tvUserName"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="39dp"
android:layout_marginEnd="39dp"
android:layout_marginTop="29dp"
android:textSize="20sp"
android:ellipsize="end"
android:text="USERNAME"
android:textColor="@color/colorPrimary"/>
<TextView
android:id="@+id/tvUserUrl"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="https://userurl.com"
android:layout_below="@+id/tvUserName"
android:layout_alignLeft="@+id/tvUserName"
android:layout_alignStart="@+id/tvUserName"
android:autoLink="all"/>
<TextView
android:id="@+id/tvRepos"
android:text="3 random repos:"
android:layout_below="@+id/tvUserUrl"
android:layout_alignLeft="@+id/tvUserUrl"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ListView
android:id="@+id/lvRepos"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_below="@+id/tvRepos"
android:layout_toRightOf="@+id/ivUser"/>
<ImageView
android:id="@+id/ivUser"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@mipmap/ic_launcher"
android:layout_marginRight="21dp"
android:layout_marginEnd="21dp"
android:layout_centerVertical="true"
android:layout_toLeftOf="@+id/tvUserName"
android:layout_toStartOf="@+id/tvUserName" />
</RelativeLayout>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">
</TextView>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimary">#123DE1</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="colorAccent">#22AACC</color>
</resources>