├── .gitignore ├── LICENSE ├── README.md ├── app-mvp ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── uk │ │ │ └── ivanc │ │ │ └── archimvp │ │ │ ├── ArchiApplication.java │ │ │ ├── RepositoryAdapter.java │ │ │ ├── model │ │ │ ├── GithubService.java │ │ │ ├── Repository.java │ │ │ └── User.java │ │ │ ├── presenter │ │ │ ├── MainPresenter.java │ │ │ ├── Presenter.java │ │ │ └── RepositoryPresenter.java │ │ │ └── view │ │ │ ├── MainActivity.java │ │ │ ├── MainMvpView.java │ │ │ ├── MvpView.java │ │ │ ├── RepositoryActivity.java │ │ │ └── RepositoryMvpView.java │ └── res │ │ ├── drawable-hdpi │ │ └── ic_search_white_36dp.png │ │ ├── drawable-mdpi │ │ └── ic_search_white_36dp.png │ │ ├── drawable-xhdpi │ │ └── ic_search_white_36dp.png │ │ ├── drawable-xxhdpi │ │ ├── ic_search_white_36dp.png │ │ ├── octocat.png │ │ └── placeholder.png │ │ ├── drawable-xxxhdpi │ │ └── ic_search_white_36dp.png │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_repository.xml │ │ └── item_repo.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── uk │ └── ivanc │ └── archimvp │ ├── MainPresenterTest.java │ ├── RepositoryPresenterTest.java │ └── util │ └── MockModelFabric.java ├── app-mvvm ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── uk │ │ │ └── ivanc │ │ │ └── archimvvm │ │ │ ├── ArchiApplication.java │ │ │ ├── RepositoryAdapter.java │ │ │ ├── model │ │ │ ├── GithubService.java │ │ │ ├── Repository.java │ │ │ └── User.java │ │ │ ├── view │ │ │ ├── MainActivity.java │ │ │ └── RepositoryActivity.java │ │ │ └── viewmodel │ │ │ ├── ItemRepoViewModel.java │ │ │ ├── MainViewModel.java │ │ │ ├── RepositoryViewModel.java │ │ │ └── ViewModel.java │ └── res │ │ ├── drawable-hdpi │ │ └── ic_search_white_36dp.png │ │ ├── drawable-mdpi │ │ └── ic_search_white_36dp.png │ │ ├── drawable-xhdpi │ │ └── ic_search_white_36dp.png │ │ ├── drawable-xxhdpi │ │ ├── ic_search_white_36dp.png │ │ ├── octocat.png │ │ └── placeholder.png │ │ ├── drawable-xxxhdpi │ │ └── ic_search_white_36dp.png │ │ ├── layout │ │ ├── item_repo.xml │ │ ├── main_activity.xml │ │ └── repository_activity.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── uk │ └── ivanc │ └── archimvvm │ ├── ItemRepoViewModelTest.java │ ├── MainViewModelTest.java │ ├── RepositoryViewModelTest.java │ └── util │ └── MockModelFabric.java ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── uk │ │ └── ivanc │ │ └── archi │ │ ├── ArchiApplication.java │ │ ├── MainActivity.java │ │ ├── RepositoryActivity.java │ │ ├── RepositoryAdapter.java │ │ └── model │ │ ├── GithubService.java │ │ ├── Repository.java │ │ └── User.java │ └── res │ ├── drawable-hdpi │ └── ic_search_white_36dp.png │ ├── drawable-mdpi │ └── ic_search_white_36dp.png │ ├── drawable-xhdpi │ └── ic_search_white_36dp.png │ ├── drawable-xxhdpi │ ├── ic_search_white_36dp.png │ ├── octocat.png │ └── placeholder.png │ ├── drawable-xxxhdpi │ └── ic_search_white_36dp.png │ ├── layout │ ├── activity_main.xml │ ├── activity_repository.xml │ └── item_repo.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── dependencies.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images └── archi-screenshots.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea 4 | *.iml 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Archi 2 | This repository showcases and compares different architectural patterns that can be used to build Android apps. The exact same sample app is built three times using the following approaches: 3 | * __Standard Android__: traditional approach with layouts, Activities/Fragments and Model. 4 | * __MVP__: Model View Presenter. 5 | * __MVVM__: Model View ViewModel with data binding. 6 | 7 | ## The App 8 | 9 | The sample app displays a list of GitHub public repositories for a given username. Tapping on one of them will open a repository details screen, where more information about the repo can be found. This screen also shows information about the owner of the repository. 10 | 11 | ![Screenshots](images/archi-screenshots.png) 12 | 13 | ### Libraries used 14 | * AppCompat, CardView and RecyclerView 15 | * Data Binding (only MVVM) 16 | * RxJava & RxAndroid 17 | * Retrofit 2 18 | * Picasso 19 | * Mockito 20 | * Robolectric 21 | 22 | ## Standard Android 23 | The `/app` directoy contains the implementation that follows the traditional standard Android approach. This is a couple of layout files, two Activities and the model. The model is exactly the same for the three implementations and it contains: `Repository`, `User` and a retrofit service (`GithubService`). 24 | 25 | With this approach, Activities are in charge of calling the `GithubService`, processing the data and updating the views. They act kind of like a controller in MVC but with some extra responsibilities that should be part of the view. The problem with this standard architecture is that Activities and Fragments can become quite large and very difficult to tests. Hence why I didn't write any unit test for this case. 26 | 27 | ## MVP - Model View Presenter 28 | In `/app-mvp` you will find the sample app implemented following this pattern. When using mvp, Activities and Fragments become part of the view layer and they delegate most of the work to presenters. Each Activity has a matching presenter that handles accessing the model via the `GithubService`. They also notify the Activities when the data is ready to display. Unit testing presenters becomes very easy by mocking the view layer (Activities). 29 | 30 | ## MVVM - Model View ViewModel 31 | This pattern has recently started to gain popularity due to the release of the [data binding library](https://developer.android.com/tools/data-binding/guide.html). You will find the implementation in `/app-mvvm`. In this case, ViewModels retrieve data from the model when requested from the view via data binding. With this pattern, Activities and Fragments become very lightweight. Moreover, writting unit tests becomes easier because the ViewModels are decoupled from the view. 32 | 33 | ## License 34 | 35 | ``` 36 | Licensed under the Apache License, Version 2.0 (the "License"); 37 | you may not use this file except in compliance with the License. 38 | You may obtain a copy of the License at 39 | 40 | http://www.apache.org/licenses/LICENSE-2.0 41 | 42 | Unless required by applicable law or agreed to in writing, software 43 | distributed under the License is distributed on an "AS IS" BASIS, 44 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 45 | See the License for the specific language governing permissions and 46 | limitations under the License. 47 | ``` 48 | -------------------------------------------------------------------------------- /app-mvp/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app-mvp/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion rootProject.ext.androidCompileSdkVersion 5 | buildToolsVersion rootProject.ext.androidBuildToolsVersion 6 | 7 | defaultConfig { 8 | applicationId "uk.ivanc.archimvp" 9 | minSdkVersion rootProject.ext.androidMinSdkVersion 10 | targetSdkVersion rootProject.ext.androidTargetSdkVersion 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | Map dependencies = rootProject.ext.dependencies; 24 | 25 | compile dependencies.appCompat 26 | compile dependencies.cardView 27 | compile dependencies.recyclerView 28 | compile dependencies.retrofit 29 | compile dependencies.retrofitConverterGson 30 | compile dependencies.retrofitAdapterRxJava 31 | compile dependencies.picasso 32 | compile dependencies.rxAndroid 33 | compile dependencies.circleImageView 34 | 35 | testCompile dependencies.jUnit 36 | testCompile dependencies.mockito 37 | testCompile dependencies.robolectric 38 | } -------------------------------------------------------------------------------- /app-mvp/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/ivan/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app-mvp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app-mvp/src/main/java/uk/ivanc/archimvp/ArchiApplication.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import rx.Scheduler; 7 | import rx.schedulers.Schedulers; 8 | import uk.ivanc.archimvp.model.GithubService; 9 | 10 | public class ArchiApplication extends Application { 11 | 12 | private GithubService githubService; 13 | private Scheduler defaultSubscribeScheduler; 14 | 15 | public static ArchiApplication get(Context context) { 16 | return (ArchiApplication) context.getApplicationContext(); 17 | } 18 | 19 | public GithubService getGithubService() { 20 | if (githubService == null) { 21 | githubService = GithubService.Factory.create(); 22 | } 23 | return githubService; 24 | } 25 | 26 | //For setting mocks during testing 27 | public void setGithubService(GithubService githubService) { 28 | this.githubService = githubService; 29 | } 30 | 31 | public Scheduler defaultSubscribeScheduler() { 32 | if (defaultSubscribeScheduler == null) { 33 | defaultSubscribeScheduler = Schedulers.io(); 34 | } 35 | return defaultSubscribeScheduler; 36 | } 37 | 38 | //User to change scheduler from tests 39 | public void setDefaultSubscribeScheduler(Scheduler scheduler) { 40 | this.defaultSubscribeScheduler = scheduler; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app-mvp/src/main/java/uk/ivanc/archimvp/RepositoryAdapter.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.TextView; 9 | 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | import uk.ivanc.archimvp.model.Repository; 14 | 15 | public class RepositoryAdapter extends RecyclerView.Adapter { 16 | 17 | private List repositories; 18 | private Callback callback; 19 | 20 | public RepositoryAdapter() { 21 | this.repositories = Collections.emptyList(); 22 | } 23 | 24 | public RepositoryAdapter(List repositories) { 25 | this.repositories = repositories; 26 | } 27 | 28 | public void setRepositories(List repositories) { 29 | this.repositories = repositories; 30 | } 31 | 32 | public void setCallback(Callback callback) { 33 | this.callback = callback; 34 | } 35 | 36 | @Override 37 | public RepositoryViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 38 | final View itemView = LayoutInflater.from(parent.getContext()) 39 | .inflate(R.layout.item_repo, parent, false); 40 | final RepositoryViewHolder viewHolder = new RepositoryViewHolder(itemView); 41 | viewHolder.contentLayout.setOnClickListener(new View.OnClickListener() { 42 | @Override 43 | public void onClick(View v) { 44 | if (callback != null) { 45 | callback.onItemClick(viewHolder.repository); 46 | } 47 | } 48 | }); 49 | return viewHolder; 50 | } 51 | 52 | @Override 53 | public void onBindViewHolder(RepositoryViewHolder holder, int position) { 54 | Repository repository = repositories.get(position); 55 | Context context = holder.titleTextView.getContext(); 56 | holder.repository = repository; 57 | holder.titleTextView.setText(repository.name); 58 | holder.descriptionTextView.setText(repository.description); 59 | holder.watchersTextView.setText( 60 | context.getResources().getString(R.string.text_watchers, repository.watchers)); 61 | holder.starsTextView.setText( 62 | context.getResources().getString(R.string.text_stars, repository.stars)); 63 | holder.forksTextView.setText( 64 | context.getResources().getString(R.string.text_forks, repository.forks)); 65 | } 66 | 67 | @Override 68 | public int getItemCount() { 69 | return repositories.size(); 70 | } 71 | 72 | public static class RepositoryViewHolder extends RecyclerView.ViewHolder { 73 | public View contentLayout; 74 | public TextView titleTextView; 75 | public TextView descriptionTextView; 76 | public TextView watchersTextView; 77 | public TextView starsTextView; 78 | public TextView forksTextView; 79 | public Repository repository; 80 | 81 | public RepositoryViewHolder(View itemView) { 82 | super(itemView); 83 | contentLayout = itemView.findViewById(R.id.layout_content); 84 | titleTextView = (TextView) itemView.findViewById(R.id.text_repo_title); 85 | descriptionTextView = (TextView) itemView.findViewById(R.id.text_repo_description); 86 | watchersTextView = (TextView) itemView.findViewById(R.id.text_watchers); 87 | starsTextView = (TextView) itemView.findViewById(R.id.text_stars); 88 | forksTextView = (TextView) itemView.findViewById(R.id.text_forks); 89 | } 90 | } 91 | 92 | public interface Callback { 93 | void onItemClick(Repository repository); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app-mvp/src/main/java/uk/ivanc/archimvp/model/GithubService.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp.model; 2 | 3 | import java.util.List; 4 | 5 | import retrofit2.Retrofit; 6 | import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory; 7 | import retrofit2.converter.gson.GsonConverterFactory; 8 | import retrofit2.http.GET; 9 | import retrofit2.http.Path; 10 | import retrofit2.http.Url; 11 | import rx.Observable; 12 | 13 | public interface GithubService { 14 | 15 | @GET("users/{username}/repos") 16 | Observable> publicRepositories(@Path("username") String username); 17 | 18 | @GET 19 | Observable userFromUrl(@Url String userUrl); 20 | 21 | 22 | class Factory { 23 | public static GithubService create() { 24 | Retrofit retrofit = new Retrofit.Builder() 25 | .baseUrl("https://api.github.com/") 26 | .addConverterFactory(GsonConverterFactory.create()) 27 | .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) 28 | .build(); 29 | return retrofit.create(GithubService.class); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app-mvp/src/main/java/uk/ivanc/archimvp/model/Repository.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp.model; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | public class Repository implements Parcelable { 9 | public long id; 10 | public String name; 11 | public String description; 12 | public int forks; 13 | public int watchers; 14 | @SerializedName("stargazers_count") 15 | public int stars; 16 | public String language; 17 | public String homepage; 18 | public User owner; 19 | public boolean fork; 20 | 21 | public Repository() { 22 | } 23 | 24 | public boolean hasHomepage() { 25 | return homepage != null && !homepage.isEmpty(); 26 | } 27 | 28 | public boolean hasLanguage() { 29 | return language != null && !language.isEmpty(); 30 | } 31 | 32 | public boolean isFork() { 33 | return fork; 34 | } 35 | 36 | @Override 37 | public int describeContents() { 38 | return 0; 39 | } 40 | 41 | @Override 42 | public void writeToParcel(Parcel dest, int flags) { 43 | dest.writeLong(this.id); 44 | dest.writeString(this.name); 45 | dest.writeString(this.description); 46 | dest.writeInt(this.forks); 47 | dest.writeInt(this.watchers); 48 | dest.writeInt(this.stars); 49 | dest.writeString(this.language); 50 | dest.writeString(this.homepage); 51 | dest.writeParcelable(this.owner, 0); 52 | dest.writeByte(fork ? (byte) 1 : (byte) 0); 53 | } 54 | 55 | protected Repository(Parcel in) { 56 | this.id = in.readLong(); 57 | this.name = in.readString(); 58 | this.description = in.readString(); 59 | this.forks = in.readInt(); 60 | this.watchers = in.readInt(); 61 | this.stars = in.readInt(); 62 | this.language = in.readString(); 63 | this.homepage = in.readString(); 64 | this.owner = in.readParcelable(User.class.getClassLoader()); 65 | this.fork = in.readByte() != 0; 66 | } 67 | 68 | public static final Creator CREATOR = new Creator() { 69 | public Repository createFromParcel(Parcel source) { 70 | return new Repository(source); 71 | } 72 | 73 | public Repository[] newArray(int size) { 74 | return new Repository[size]; 75 | } 76 | }; 77 | 78 | @Override 79 | public boolean equals(Object o) { 80 | if (this == o) return true; 81 | if (o == null || getClass() != o.getClass()) return false; 82 | 83 | Repository that = (Repository) o; 84 | 85 | if (id != that.id) return false; 86 | if (forks != that.forks) return false; 87 | if (watchers != that.watchers) return false; 88 | if (stars != that.stars) return false; 89 | if (fork != that.fork) return false; 90 | if (name != null ? !name.equals(that.name) : that.name != null) return false; 91 | if (description != null ? !description.equals(that.description) : that.description != null) 92 | return false; 93 | if (language != null ? !language.equals(that.language) : that.language != null) 94 | return false; 95 | if (homepage != null ? !homepage.equals(that.homepage) : that.homepage != null) 96 | return false; 97 | return !(owner != null ? !owner.equals(that.owner) : that.owner != null); 98 | 99 | } 100 | 101 | @Override 102 | public int hashCode() { 103 | int result = (int) (id ^ (id >>> 32)); 104 | result = 31 * result + (name != null ? name.hashCode() : 0); 105 | result = 31 * result + (description != null ? description.hashCode() : 0); 106 | result = 31 * result + forks; 107 | result = 31 * result + watchers; 108 | result = 31 * result + stars; 109 | result = 31 * result + (language != null ? language.hashCode() : 0); 110 | result = 31 * result + (homepage != null ? homepage.hashCode() : 0); 111 | result = 31 * result + (owner != null ? owner.hashCode() : 0); 112 | result = 31 * result + (fork ? 1 : 0); 113 | return result; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app-mvp/src/main/java/uk/ivanc/archimvp/model/User.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp.model; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | public class User implements Parcelable { 9 | public long id; 10 | public String name; 11 | public String url; 12 | public String email; 13 | public String login; 14 | public String location; 15 | @SerializedName("avatar_url") 16 | public String avatarUrl; 17 | 18 | public User() { 19 | } 20 | 21 | public boolean hasEmail() { 22 | return email != null && !email.isEmpty(); 23 | } 24 | 25 | public boolean hasLocation() { 26 | return location != null && !location.isEmpty(); 27 | } 28 | 29 | @Override 30 | public int describeContents() { 31 | return 0; 32 | } 33 | 34 | @Override 35 | public void writeToParcel(Parcel dest, int flags) { 36 | dest.writeLong(this.id); 37 | dest.writeString(this.name); 38 | dest.writeString(this.url); 39 | dest.writeString(this.email); 40 | dest.writeString(this.login); 41 | dest.writeString(this.location); 42 | dest.writeString(this.avatarUrl); 43 | } 44 | 45 | protected User(Parcel in) { 46 | this.id = in.readLong(); 47 | this.name = in.readString(); 48 | this.url = in.readString(); 49 | this.email = in.readString(); 50 | this.login = in.readString(); 51 | this.location = in.readString(); 52 | this.avatarUrl = in.readString(); 53 | } 54 | 55 | public static final Creator CREATOR = new Creator() { 56 | public User createFromParcel(Parcel source) { 57 | return new User(source); 58 | } 59 | 60 | public User[] newArray(int size) { 61 | return new User[size]; 62 | } 63 | }; 64 | 65 | @Override 66 | public boolean equals(Object o) { 67 | if (this == o) return true; 68 | if (o == null || getClass() != o.getClass()) return false; 69 | 70 | User user = (User) o; 71 | 72 | if (id != user.id) return false; 73 | if (name != null ? !name.equals(user.name) : user.name != null) return false; 74 | if (url != null ? !url.equals(user.url) : user.url != null) return false; 75 | if (email != null ? !email.equals(user.email) : user.email != null) return false; 76 | if (login != null ? !login.equals(user.login) : user.login != null) return false; 77 | if (location != null ? !location.equals(user.location) : user.location != null) 78 | return false; 79 | return !(avatarUrl != null ? !avatarUrl.equals(user.avatarUrl) : user.avatarUrl != null); 80 | 81 | } 82 | 83 | @Override 84 | public int hashCode() { 85 | int result = (int) (id ^ (id >>> 32)); 86 | result = 31 * result + (name != null ? name.hashCode() : 0); 87 | result = 31 * result + (url != null ? url.hashCode() : 0); 88 | result = 31 * result + (email != null ? email.hashCode() : 0); 89 | result = 31 * result + (login != null ? login.hashCode() : 0); 90 | result = 31 * result + (location != null ? location.hashCode() : 0); 91 | result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0); 92 | return result; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app-mvp/src/main/java/uk/ivanc/archimvp/presenter/MainPresenter.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp.presenter; 2 | 3 | import android.util.Log; 4 | 5 | import java.util.List; 6 | 7 | import retrofit2.adapter.rxjava.HttpException; 8 | import rx.Subscriber; 9 | import rx.Subscription; 10 | import rx.android.schedulers.AndroidSchedulers; 11 | import uk.ivanc.archimvp.ArchiApplication; 12 | import uk.ivanc.archimvp.R; 13 | import uk.ivanc.archimvp.model.GithubService; 14 | import uk.ivanc.archimvp.model.Repository; 15 | import uk.ivanc.archimvp.view.MainMvpView; 16 | 17 | public class MainPresenter implements Presenter { 18 | 19 | public static String TAG = "MainPresenter"; 20 | 21 | private MainMvpView mainMvpView; 22 | private Subscription subscription; 23 | private List repositories; 24 | 25 | @Override 26 | public void attachView(MainMvpView view) { 27 | this.mainMvpView = view; 28 | } 29 | 30 | @Override 31 | public void detachView() { 32 | this.mainMvpView = null; 33 | if (subscription != null) subscription.unsubscribe(); 34 | } 35 | 36 | public void loadRepositories(String usernameEntered) { 37 | String username = usernameEntered.trim(); 38 | if (username.isEmpty()) return; 39 | 40 | mainMvpView.showProgressIndicator(); 41 | if (subscription != null) subscription.unsubscribe(); 42 | ArchiApplication application = ArchiApplication.get(mainMvpView.getContext()); 43 | GithubService githubService = application.getGithubService(); 44 | subscription = githubService.publicRepositories(username) 45 | .observeOn(AndroidSchedulers.mainThread()) 46 | .subscribeOn(application.defaultSubscribeScheduler()) 47 | .subscribe(new Subscriber>() { 48 | @Override 49 | public void onCompleted() { 50 | Log.i(TAG, "Repos loaded " + repositories); 51 | if (!repositories.isEmpty()) { 52 | mainMvpView.showRepositories(repositories); 53 | } else { 54 | mainMvpView.showMessage(R.string.text_empty_repos); 55 | } 56 | } 57 | 58 | @Override 59 | public void onError(Throwable error) { 60 | Log.e(TAG, "Error loading GitHub repos ", error); 61 | if (isHttp404(error)) { 62 | mainMvpView.showMessage(R.string.error_username_not_found); 63 | } else { 64 | mainMvpView.showMessage(R.string.error_loading_repos); 65 | } 66 | } 67 | 68 | @Override 69 | public void onNext(List repositories) { 70 | MainPresenter.this.repositories = repositories; 71 | } 72 | }); 73 | } 74 | 75 | private static boolean isHttp404(Throwable error) { 76 | return error instanceof HttpException && ((HttpException) error).code() == 404; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /app-mvp/src/main/java/uk/ivanc/archimvp/presenter/Presenter.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp.presenter; 2 | 3 | public interface Presenter { 4 | 5 | void attachView(V view); 6 | 7 | void detachView(); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app-mvp/src/main/java/uk/ivanc/archimvp/presenter/RepositoryPresenter.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp.presenter; 2 | 3 | import android.util.Log; 4 | 5 | import rx.Subscription; 6 | import rx.android.schedulers.AndroidSchedulers; 7 | import rx.functions.Action1; 8 | import uk.ivanc.archimvp.ArchiApplication; 9 | import uk.ivanc.archimvp.model.GithubService; 10 | import uk.ivanc.archimvp.model.User; 11 | import uk.ivanc.archimvp.view.RepositoryMvpView; 12 | 13 | public class RepositoryPresenter implements Presenter { 14 | 15 | private static final String TAG = "RepositoryPresenter"; 16 | 17 | private RepositoryMvpView repositoryMvpView; 18 | private Subscription subscription; 19 | 20 | @Override 21 | public void attachView(RepositoryMvpView view) { 22 | this.repositoryMvpView = view; 23 | } 24 | 25 | @Override 26 | public void detachView() { 27 | this.repositoryMvpView = null; 28 | if (subscription != null) subscription.unsubscribe(); 29 | } 30 | 31 | public void loadOwner(String userUrl) { 32 | ArchiApplication application = ArchiApplication.get(repositoryMvpView.getContext()); 33 | GithubService githubService = application.getGithubService(); 34 | subscription = githubService.userFromUrl(userUrl) 35 | .observeOn(AndroidSchedulers.mainThread()) 36 | .subscribeOn(application.defaultSubscribeScheduler()) 37 | .subscribe(new Action1() { 38 | @Override 39 | public void call(User user) { 40 | Log.i(TAG, "Full user data loaded " + user); 41 | repositoryMvpView.showOwner(user); 42 | } 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app-mvp/src/main/java/uk/ivanc/archimvp/view/MainActivity.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp.view; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.support.v7.widget.Toolbar; 9 | import android.text.Editable; 10 | import android.text.TextWatcher; 11 | import android.view.KeyEvent; 12 | import android.view.View; 13 | import android.view.inputmethod.EditorInfo; 14 | import android.view.inputmethod.InputMethodManager; 15 | import android.widget.EditText; 16 | import android.widget.ImageButton; 17 | import android.widget.ProgressBar; 18 | import android.widget.TextView; 19 | 20 | import java.util.List; 21 | 22 | import uk.ivanc.archimvp.R; 23 | import uk.ivanc.archimvp.RepositoryAdapter; 24 | import uk.ivanc.archimvp.model.Repository; 25 | import uk.ivanc.archimvp.presenter.MainPresenter; 26 | 27 | public class MainActivity extends AppCompatActivity implements MainMvpView { 28 | 29 | private MainPresenter presenter; 30 | 31 | private RecyclerView reposRecycleView; 32 | private Toolbar toolbar; 33 | private EditText editTextUsername; 34 | private ProgressBar progressBar; 35 | private TextView infoTextView; 36 | private ImageButton searchButton; 37 | 38 | @Override 39 | protected void onCreate(Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | //Set up presenter 42 | presenter = new MainPresenter(); 43 | presenter.attachView(this); 44 | 45 | setContentView(R.layout.activity_main); 46 | progressBar = (ProgressBar) findViewById(R.id.progress); 47 | infoTextView = (TextView) findViewById(R.id.text_info); 48 | //Set up ToolBar 49 | toolbar = (Toolbar) findViewById(R.id.toolbar); 50 | setSupportActionBar(toolbar); 51 | //Set up RecyclerView 52 | reposRecycleView = (RecyclerView) findViewById(R.id.repos_recycler_view); 53 | setupRecyclerView(reposRecycleView); 54 | // Set up search button 55 | searchButton = (ImageButton) findViewById(R.id.button_search); 56 | searchButton.setOnClickListener(new View.OnClickListener() { 57 | @Override 58 | public void onClick(View v) { 59 | presenter.loadRepositories(editTextUsername.getText().toString()); 60 | } 61 | }); 62 | //Set up username EditText 63 | editTextUsername = (EditText) findViewById(R.id.edit_text_username); 64 | editTextUsername.addTextChangedListener(mHideShowButtonTextWatcher); 65 | editTextUsername.setOnEditorActionListener(new TextView.OnEditorActionListener() { 66 | @Override 67 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 68 | if (actionId == EditorInfo.IME_ACTION_SEARCH) { 69 | presenter.loadRepositories(editTextUsername.getText().toString()); 70 | return true; 71 | } 72 | return false; 73 | } 74 | }); 75 | } 76 | 77 | // MainMvpView interface methods implementation 78 | 79 | @Override 80 | protected void onDestroy() { 81 | presenter.detachView(); 82 | super.onDestroy(); 83 | } 84 | 85 | @Override 86 | public Context getContext() { 87 | return this; 88 | } 89 | 90 | @Override 91 | public void showRepositories(List repositories) { 92 | RepositoryAdapter adapter = (RepositoryAdapter) reposRecycleView.getAdapter(); 93 | adapter.setRepositories(repositories); 94 | adapter.notifyDataSetChanged(); 95 | reposRecycleView.requestFocus(); 96 | hideSoftKeyboard(); 97 | progressBar.setVisibility(View.INVISIBLE); 98 | infoTextView.setVisibility(View.INVISIBLE); 99 | reposRecycleView.setVisibility(View.VISIBLE); 100 | } 101 | 102 | @Override 103 | public void showMessage(int stringId) { 104 | progressBar.setVisibility(View.INVISIBLE); 105 | infoTextView.setVisibility(View.VISIBLE); 106 | reposRecycleView.setVisibility(View.INVISIBLE); 107 | infoTextView.setText(getString(stringId)); 108 | } 109 | 110 | @Override 111 | public void showProgressIndicator() { 112 | progressBar.setVisibility(View.VISIBLE); 113 | infoTextView.setVisibility(View.INVISIBLE); 114 | reposRecycleView.setVisibility(View.INVISIBLE); 115 | } 116 | 117 | private void setupRecyclerView(RecyclerView recyclerView) { 118 | RepositoryAdapter adapter = new RepositoryAdapter(); 119 | adapter.setCallback(new RepositoryAdapter.Callback() { 120 | @Override 121 | public void onItemClick(Repository repository) { 122 | startActivity(RepositoryActivity.newIntent(MainActivity.this, repository)); 123 | } 124 | }); 125 | recyclerView.setAdapter(adapter); 126 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 127 | } 128 | 129 | private void hideSoftKeyboard() { 130 | InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); 131 | imm.hideSoftInputFromWindow(editTextUsername.getWindowToken(), 0); 132 | } 133 | 134 | private TextWatcher mHideShowButtonTextWatcher = new TextWatcher() { 135 | @Override 136 | public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { 137 | 138 | } 139 | 140 | @Override 141 | public void onTextChanged(CharSequence charSequence, int start, int before, int count) { 142 | searchButton.setVisibility(charSequence.length() > 0 ? View.VISIBLE : View.GONE); 143 | } 144 | 145 | @Override 146 | public void afterTextChanged(Editable editable) { 147 | 148 | } 149 | }; 150 | 151 | } 152 | -------------------------------------------------------------------------------- /app-mvp/src/main/java/uk/ivanc/archimvp/view/MainMvpView.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp.view; 2 | 3 | import java.util.List; 4 | 5 | import uk.ivanc.archimvp.model.Repository; 6 | 7 | public interface MainMvpView extends MvpView { 8 | 9 | void showRepositories(List repositories); 10 | 11 | void showMessage(int stringId); 12 | 13 | void showProgressIndicator(); 14 | } 15 | -------------------------------------------------------------------------------- /app-mvp/src/main/java/uk/ivanc/archimvp/view/MvpView.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp.view; 2 | 3 | import android.content.Context; 4 | 5 | public interface MvpView { 6 | 7 | Context getContext(); 8 | } 9 | -------------------------------------------------------------------------------- /app-mvp/src/main/java/uk/ivanc/archimvp/view/RepositoryActivity.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp.view; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v7.app.ActionBar; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.support.v7.widget.Toolbar; 9 | import android.view.View; 10 | import android.widget.ImageView; 11 | import android.widget.TextView; 12 | 13 | import com.squareup.picasso.Picasso; 14 | 15 | import uk.ivanc.archimvp.R; 16 | import uk.ivanc.archimvp.model.Repository; 17 | import uk.ivanc.archimvp.model.User; 18 | import uk.ivanc.archimvp.presenter.RepositoryPresenter; 19 | 20 | public class RepositoryActivity extends AppCompatActivity implements RepositoryMvpView { 21 | 22 | private static final String EXTRA_REPOSITORY = "EXTRA_REPOSITORY"; 23 | 24 | private Toolbar toolbar; 25 | private TextView descriptionText; 26 | private TextView homepageText; 27 | private TextView languageText; 28 | private TextView forkText; 29 | private TextView ownerNameText; 30 | private TextView ownerEmailText; 31 | private TextView ownerLocationText; 32 | private ImageView ownerImage; 33 | private View ownerLayout; 34 | 35 | private RepositoryPresenter presenter; 36 | 37 | public static Intent newIntent(Context context, Repository repository) { 38 | Intent intent = new Intent(context, RepositoryActivity.class); 39 | intent.putExtra(EXTRA_REPOSITORY, repository); 40 | return intent; 41 | } 42 | 43 | @Override 44 | protected void onCreate(Bundle savedInstanceState) { 45 | super.onCreate(savedInstanceState); 46 | presenter = new RepositoryPresenter(); 47 | presenter.attachView(this); 48 | 49 | setContentView(R.layout.activity_repository); 50 | toolbar = (Toolbar) findViewById(R.id.toolbar); 51 | descriptionText = (TextView) findViewById(R.id.text_repo_description); 52 | homepageText = (TextView) findViewById(R.id.text_homepage); 53 | languageText = (TextView) findViewById(R.id.text_language); 54 | forkText = (TextView) findViewById(R.id.text_fork); 55 | ownerNameText = (TextView) findViewById(R.id.text_owner_name); 56 | ownerEmailText = (TextView) findViewById(R.id.text_owner_email); 57 | ownerLocationText = (TextView) findViewById(R.id.text_owner_location); 58 | ownerImage = (ImageView) findViewById(R.id.image_owner); 59 | ownerLayout = findViewById(R.id.layout_owner); 60 | 61 | setSupportActionBar(toolbar); 62 | ActionBar actionBar = getSupportActionBar(); 63 | if (actionBar != null) { 64 | actionBar.setDisplayHomeAsUpEnabled(true); 65 | } 66 | 67 | Repository repository = getIntent().getParcelableExtra(EXTRA_REPOSITORY); 68 | bindRepositoryData(repository); 69 | presenter.loadOwner(repository.owner.url); 70 | } 71 | 72 | @Override 73 | protected void onDestroy() { 74 | super.onDestroy(); 75 | presenter.detachView(); 76 | } 77 | 78 | @Override 79 | public Context getContext() { 80 | return this; 81 | } 82 | 83 | @Override 84 | public void showOwner(final User owner) { 85 | ownerNameText.setText(owner.name); 86 | ownerEmailText.setText(owner.email); 87 | ownerEmailText.setVisibility(owner.hasEmail() ? View.VISIBLE : View.GONE); 88 | ownerLocationText.setText(owner.location); 89 | ownerLocationText.setVisibility(owner.hasLocation() ? View.VISIBLE : View.GONE); 90 | ownerLayout.setVisibility(View.VISIBLE); 91 | } 92 | 93 | private void bindRepositoryData(final Repository repository) { 94 | setTitle(repository.name); 95 | descriptionText.setText(repository.description); 96 | homepageText.setText(repository.homepage); 97 | homepageText.setVisibility(repository.hasHomepage() ? View.VISIBLE : View.GONE); 98 | languageText.setText(getString(R.string.text_language, repository.language)); 99 | languageText.setVisibility(repository.hasLanguage() ? View.VISIBLE : View.GONE); 100 | forkText.setVisibility(repository.isFork() ? View.VISIBLE : View.GONE); 101 | //Preload image for user because we already have it before loading the full user 102 | Picasso.with(this) 103 | .load(repository.owner.avatarUrl) 104 | .placeholder(R.drawable.placeholder) 105 | .into(ownerImage); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app-mvp/src/main/java/uk/ivanc/archimvp/view/RepositoryMvpView.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp.view; 2 | 3 | import uk.ivanc.archimvp.model.User; 4 | 5 | public interface RepositoryMvpView extends MvpView { 6 | 7 | void showOwner(final User owner); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app-mvp/src/main/res/drawable-hdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvp/src/main/res/drawable-hdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app-mvp/src/main/res/drawable-mdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvp/src/main/res/drawable-mdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app-mvp/src/main/res/drawable-xhdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvp/src/main/res/drawable-xhdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app-mvp/src/main/res/drawable-xxhdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvp/src/main/res/drawable-xxhdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app-mvp/src/main/res/drawable-xxhdpi/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvp/src/main/res/drawable-xxhdpi/octocat.png -------------------------------------------------------------------------------- /app-mvp/src/main/res/drawable-xxhdpi/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvp/src/main/res/drawable-xxhdpi/placeholder.png -------------------------------------------------------------------------------- /app-mvp/src/main/res/drawable-xxxhdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvp/src/main/res/drawable-xxxhdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app-mvp/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | 24 | 25 | 36 | 37 | 47 | 48 | 58 | 59 | 60 | 61 | 69 | 70 | 83 | 84 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /app-mvp/src/main/res/layout/activity_repository.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 23 | 24 | 31 | 32 | 43 | 44 | 45 | 46 | 55 | 56 | 63 | 64 | 72 | 73 | 81 | 82 | 87 | 88 | 96 | 97 | 101 | 102 | 110 | 111 | 118 | 119 | 127 | 128 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /app-mvp/src/main/res/layout/item_repo.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 20 | 21 | 32 | 33 | 44 | 45 | 49 | 50 | 54 | 55 | 63 | 64 | 72 | 73 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /app-mvp/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvp/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-mvp/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvp/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-mvp/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvp/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-mvp/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvp/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-mvp/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | #ffffff 5 | #75ffffff 6 | #e1e1e1 7 | #3F51B5 8 | #303F9F 9 | #C5CAE9 10 | #03A9F4 11 | #212121 12 | #727272 13 | #FFFFFF 14 | #cbcbcb 15 | -------------------------------------------------------------------------------- /app-mvp/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12dp 4 | 12dp 5 | 6dp 6 | 6dp 7 | 8 | -------------------------------------------------------------------------------- /app-mvp/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Archi MVP 3 | 4 | Hello world! 5 | Settings 6 | %d \nStars 7 | %d \nWatchers 8 | %d \nForks 9 | Oops, something went wrong 10 | This account doesn\'t have any public repository 11 | Oops, Octocat doesn\'t know that username 12 | GitHub username 13 | Enter a GitHub username above to see its repositories 14 | This repository is a fork 15 | Language: %s 16 | 17 | -------------------------------------------------------------------------------- /app-mvp/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app-mvp/src/test/java/uk/ivanc/archimvp/MainPresenterTest.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.robolectric.RobolectricGradleTestRunner; 8 | import org.robolectric.RuntimeEnvironment; 9 | import org.robolectric.annotation.Config; 10 | 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | import okhttp3.ResponseBody; 15 | import retrofit2.Response; 16 | import retrofit2.adapter.rxjava.HttpException; 17 | import rx.Observable; 18 | import rx.schedulers.Schedulers; 19 | import uk.ivanc.archimvp.model.GithubService; 20 | import uk.ivanc.archimvp.model.Repository; 21 | import uk.ivanc.archimvp.presenter.MainPresenter; 22 | import uk.ivanc.archimvp.util.MockModelFabric; 23 | import uk.ivanc.archimvp.view.MainMvpView; 24 | 25 | import static org.mockito.Mockito.mock; 26 | import static org.mockito.Mockito.verify; 27 | import static org.mockito.Mockito.when; 28 | 29 | @RunWith(RobolectricGradleTestRunner.class) 30 | @Config(constants = BuildConfig.class, sdk = 21) 31 | public class MainPresenterTest { 32 | 33 | MainPresenter mainPresenter; 34 | MainMvpView mainMvpView; 35 | GithubService githubService; 36 | 37 | @Before 38 | public void setUp() { 39 | ArchiApplication application = (ArchiApplication) RuntimeEnvironment.application; 40 | githubService = mock(GithubService.class); 41 | // Mock the retrofit service so we don't call the API directly 42 | application.setGithubService(githubService); 43 | // Change the default subscribe schedulers so all observables 44 | // will now run on the same thread 45 | application.setDefaultSubscribeScheduler(Schedulers.immediate()); 46 | mainPresenter = new MainPresenter(); 47 | mainMvpView = mock(MainMvpView.class); 48 | when(mainMvpView.getContext()).thenReturn(application); 49 | mainPresenter.attachView(mainMvpView); 50 | } 51 | 52 | @After 53 | public void tearDown() { 54 | mainPresenter.detachView(); 55 | } 56 | 57 | @Test 58 | public void loadRepositoriesCallsShowRepositories() { 59 | String username = "ivacf"; 60 | List repositories = MockModelFabric.newListOfRepositories(10); 61 | when(githubService.publicRepositories(username)) 62 | .thenReturn(Observable.just(repositories)); 63 | 64 | mainPresenter.loadRepositories(username); 65 | verify(mainMvpView).showProgressIndicator(); 66 | verify(mainMvpView).showRepositories(repositories); 67 | } 68 | 69 | @Test 70 | public void loadRepositoriesCallsShowMessage_withEmptyReposString() { 71 | String username = "ivacf"; 72 | when(githubService.publicRepositories(username)) 73 | .thenReturn(Observable.just(Collections.emptyList())); 74 | 75 | mainPresenter.loadRepositories(username); 76 | verify(mainMvpView).showProgressIndicator(); 77 | verify(mainMvpView).showMessage(R.string.text_empty_repos); 78 | } 79 | 80 | @Test 81 | public void loadRepositoriesCallsShowMessage_withDefaultErrorString() { 82 | String username = "ivacf"; 83 | when(githubService.publicRepositories(username)) 84 | .thenReturn(Observable.>error(new RuntimeException("error"))); 85 | 86 | mainPresenter.loadRepositories(username); 87 | verify(mainMvpView).showProgressIndicator(); 88 | verify(mainMvpView).showMessage(R.string.error_loading_repos); 89 | } 90 | 91 | @Test 92 | public void loadRepositoriesCallsShowMessage_withUsernameNotFoundString() { 93 | String username = "ivacf"; 94 | HttpException mockHttpException = 95 | new HttpException(Response.error(404, mock(ResponseBody.class))); 96 | when(githubService.publicRepositories(username)) 97 | .thenReturn(Observable.>error(mockHttpException)); 98 | 99 | mainPresenter.loadRepositories(username); 100 | verify(mainMvpView).showProgressIndicator(); 101 | verify(mainMvpView).showMessage(R.string.error_username_not_found); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app-mvp/src/test/java/uk/ivanc/archimvp/RepositoryPresenterTest.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.robolectric.RobolectricGradleTestRunner; 8 | import org.robolectric.RuntimeEnvironment; 9 | import org.robolectric.annotation.Config; 10 | 11 | import rx.Observable; 12 | import rx.schedulers.Schedulers; 13 | import uk.ivanc.archimvp.model.GithubService; 14 | import uk.ivanc.archimvp.model.User; 15 | import uk.ivanc.archimvp.presenter.RepositoryPresenter; 16 | import uk.ivanc.archimvp.util.MockModelFabric; 17 | import uk.ivanc.archimvp.view.RepositoryMvpView; 18 | 19 | import static org.mockito.Mockito.mock; 20 | import static org.mockito.Mockito.verify; 21 | import static org.mockito.Mockito.when; 22 | 23 | @RunWith(RobolectricGradleTestRunner.class) 24 | @Config(constants = BuildConfig.class, sdk = 21) 25 | public class RepositoryPresenterTest { 26 | RepositoryPresenter repositoryPresenter; 27 | RepositoryMvpView repositoryMvpView; 28 | GithubService githubService; 29 | 30 | @Before 31 | public void setUp() { 32 | ArchiApplication application = (ArchiApplication) RuntimeEnvironment.application; 33 | githubService = mock(GithubService.class); 34 | // Mock the retrofit service so we don't call the API directly 35 | application.setGithubService(githubService); 36 | // Change the default subscribe schedulers so all observables 37 | // will now run on the same thread 38 | application.setDefaultSubscribeScheduler(Schedulers.immediate()); 39 | repositoryPresenter = new RepositoryPresenter(); 40 | repositoryMvpView = mock(RepositoryMvpView.class); 41 | when(repositoryMvpView.getContext()).thenReturn(application); 42 | repositoryPresenter.attachView(repositoryMvpView); 43 | } 44 | 45 | @After 46 | public void tearDown() { 47 | repositoryPresenter.detachView(); 48 | } 49 | 50 | @Test 51 | public void loadOwnerCallsShowOwner() { 52 | User owner = MockModelFabric.newUser("ivan"); 53 | String userUrl = "http://user.com/more"; 54 | when(githubService.userFromUrl(userUrl)) 55 | .thenReturn(Observable.just(owner)); 56 | 57 | repositoryPresenter.loadOwner(userUrl); 58 | verify(repositoryMvpView).showOwner(owner); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app-mvp/src/test/java/uk/ivanc/archimvp/util/MockModelFabric.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvp.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Random; 6 | 7 | import uk.ivanc.archimvp.model.Repository; 8 | import uk.ivanc.archimvp.model.User; 9 | 10 | public class MockModelFabric { 11 | 12 | public static List newListOfRepositories(int numRepos) { 13 | List repositories = new ArrayList<>(numRepos); 14 | for (int i = 0; i < numRepos; i++) { 15 | repositories.add(newRepository("Repo " + i)); 16 | } 17 | return repositories; 18 | } 19 | 20 | public static Repository newRepository(String name) { 21 | Random random = new Random(); 22 | Repository repository = new Repository(); 23 | repository.name = name; 24 | repository.id = random.nextInt(10000); 25 | repository.description = "Description for " + name; 26 | repository.watchers = random.nextInt(100); 27 | repository.forks = random.nextInt(100); 28 | repository.stars = random.nextInt(100); 29 | repository.owner = newUser("User-" + name); 30 | return repository; 31 | } 32 | 33 | public static User newUser(String name) { 34 | Random random = new Random(); 35 | User user = new User(); 36 | user.id = random.nextInt(10000); 37 | user.name = name; 38 | user.email = name + "@email.com"; 39 | user.location = "Location of " + name; 40 | user.url = "http://user.com/" + name; 41 | user.avatarUrl = "http://user.com/image/" + name; 42 | return user; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app-mvvm/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app-mvvm/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion rootProject.ext.androidCompileSdkVersion 5 | buildToolsVersion rootProject.ext.androidBuildToolsVersion 6 | 7 | defaultConfig { 8 | applicationId "uk.ivanc.archimvvm" 9 | minSdkVersion rootProject.ext.androidMinSdkVersion 10 | targetSdkVersion rootProject.ext.androidTargetSdkVersion 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | 15 | dataBinding { 16 | enabled = true 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | Map dependencies = rootProject.ext.dependencies; 29 | 30 | compile dependencies.appCompat 31 | compile dependencies.cardView 32 | compile dependencies.recyclerView 33 | compile dependencies.retrofit 34 | compile dependencies.retrofitConverterGson 35 | compile dependencies.retrofitAdapterRxJava 36 | compile dependencies.picasso 37 | compile dependencies.rxAndroid 38 | compile dependencies.circleImageView 39 | 40 | testCompile dependencies.jUnit 41 | testCompile dependencies.mockito 42 | testCompile dependencies.robolectric 43 | } 44 | -------------------------------------------------------------------------------- /app-mvvm/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/ivan/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app-mvvm/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app-mvvm/src/main/java/uk/ivanc/archimvvm/ArchiApplication.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import rx.Scheduler; 7 | import rx.schedulers.Schedulers; 8 | import uk.ivanc.archimvvm.model.GithubService; 9 | 10 | public class ArchiApplication extends Application { 11 | 12 | private GithubService githubService; 13 | private Scheduler defaultSubscribeScheduler; 14 | 15 | public static ArchiApplication get(Context context) { 16 | return (ArchiApplication) context.getApplicationContext(); 17 | } 18 | 19 | public GithubService getGithubService() { 20 | if (githubService == null) { 21 | githubService = GithubService.Factory.create(); 22 | } 23 | return githubService; 24 | } 25 | 26 | //For setting mocks during testing 27 | public void setGithubService(GithubService githubService) { 28 | this.githubService = githubService; 29 | } 30 | 31 | public Scheduler defaultSubscribeScheduler() { 32 | if (defaultSubscribeScheduler == null) { 33 | defaultSubscribeScheduler = Schedulers.io(); 34 | } 35 | return defaultSubscribeScheduler; 36 | } 37 | 38 | //User to change scheduler from tests 39 | public void setDefaultSubscribeScheduler(Scheduler scheduler) { 40 | this.defaultSubscribeScheduler = scheduler; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app-mvvm/src/main/java/uk/ivanc/archimvvm/RepositoryAdapter.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm; 2 | 3 | import android.databinding.DataBindingUtil; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.ViewGroup; 7 | 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | import uk.ivanc.archimvvm.databinding.ItemRepoBinding; 12 | import uk.ivanc.archimvvm.model.Repository; 13 | import uk.ivanc.archimvvm.viewmodel.ItemRepoViewModel; 14 | 15 | public class RepositoryAdapter extends RecyclerView.Adapter { 16 | 17 | private List repositories; 18 | 19 | public RepositoryAdapter() { 20 | this.repositories = Collections.emptyList(); 21 | } 22 | 23 | public RepositoryAdapter(List repositories) { 24 | this.repositories = repositories; 25 | } 26 | 27 | public void setRepositories(List repositories) { 28 | this.repositories = repositories; 29 | } 30 | 31 | @Override 32 | public RepositoryViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 33 | ItemRepoBinding binding = DataBindingUtil.inflate( 34 | LayoutInflater.from(parent.getContext()), 35 | R.layout.item_repo, 36 | parent, 37 | false); 38 | return new RepositoryViewHolder(binding); 39 | } 40 | 41 | @Override 42 | public void onBindViewHolder(RepositoryViewHolder holder, int position) { 43 | holder.bindRepository(repositories.get(position)); 44 | } 45 | 46 | @Override 47 | public int getItemCount() { 48 | return repositories.size(); 49 | } 50 | 51 | public static class RepositoryViewHolder extends RecyclerView.ViewHolder { 52 | final ItemRepoBinding binding; 53 | 54 | public RepositoryViewHolder(ItemRepoBinding binding) { 55 | super(binding.cardView); 56 | this.binding = binding; 57 | } 58 | 59 | void bindRepository(Repository repository) { 60 | if (binding.getViewModel() == null) { 61 | binding.setViewModel(new ItemRepoViewModel(itemView.getContext(), repository)); 62 | } else { 63 | binding.getViewModel().setRepository(repository); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app-mvvm/src/main/java/uk/ivanc/archimvvm/model/GithubService.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm.model; 2 | 3 | import java.util.List; 4 | 5 | import retrofit2.Retrofit; 6 | import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory; 7 | import retrofit2.converter.gson.GsonConverterFactory; 8 | import retrofit2.http.GET; 9 | import retrofit2.http.Path; 10 | import retrofit2.http.Url; 11 | import rx.Observable; 12 | 13 | public interface GithubService { 14 | 15 | @GET("users/{username}/repos") 16 | Observable> publicRepositories(@Path("username") String username); 17 | 18 | @GET 19 | Observable userFromUrl(@Url String userUrl); 20 | 21 | 22 | class Factory { 23 | public static GithubService create() { 24 | Retrofit retrofit = new Retrofit.Builder() 25 | .baseUrl("https://api.github.com/") 26 | .addConverterFactory(GsonConverterFactory.create()) 27 | .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) 28 | .build(); 29 | return retrofit.create(GithubService.class); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app-mvvm/src/main/java/uk/ivanc/archimvvm/model/Repository.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm.model; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | public class Repository implements Parcelable { 9 | public long id; 10 | public String name; 11 | public String description; 12 | public int forks; 13 | public int watchers; 14 | @SerializedName("stargazers_count") 15 | public int stars; 16 | public String language; 17 | public String homepage; 18 | public User owner; 19 | public boolean fork; 20 | 21 | public Repository() { 22 | } 23 | 24 | public boolean hasHomepage() { 25 | return homepage != null && !homepage.isEmpty(); 26 | } 27 | 28 | public boolean hasLanguage() { 29 | return language != null && !language.isEmpty(); 30 | } 31 | 32 | public boolean isFork() { 33 | return fork; 34 | } 35 | 36 | @Override 37 | public int describeContents() { 38 | return 0; 39 | } 40 | 41 | @Override 42 | public void writeToParcel(Parcel dest, int flags) { 43 | dest.writeLong(this.id); 44 | dest.writeString(this.name); 45 | dest.writeString(this.description); 46 | dest.writeInt(this.forks); 47 | dest.writeInt(this.watchers); 48 | dest.writeInt(this.stars); 49 | dest.writeString(this.language); 50 | dest.writeString(this.homepage); 51 | dest.writeParcelable(this.owner, 0); 52 | dest.writeByte(fork ? (byte) 1 : (byte) 0); 53 | } 54 | 55 | protected Repository(Parcel in) { 56 | this.id = in.readLong(); 57 | this.name = in.readString(); 58 | this.description = in.readString(); 59 | this.forks = in.readInt(); 60 | this.watchers = in.readInt(); 61 | this.stars = in.readInt(); 62 | this.language = in.readString(); 63 | this.homepage = in.readString(); 64 | this.owner = in.readParcelable(User.class.getClassLoader()); 65 | this.fork = in.readByte() != 0; 66 | } 67 | 68 | public static final Creator CREATOR = new Creator() { 69 | public Repository createFromParcel(Parcel source) { 70 | return new Repository(source); 71 | } 72 | 73 | public Repository[] newArray(int size) { 74 | return new Repository[size]; 75 | } 76 | }; 77 | 78 | @Override 79 | public boolean equals(Object o) { 80 | if (this == o) return true; 81 | if (o == null || getClass() != o.getClass()) return false; 82 | 83 | Repository that = (Repository) o; 84 | 85 | if (id != that.id) return false; 86 | if (forks != that.forks) return false; 87 | if (watchers != that.watchers) return false; 88 | if (stars != that.stars) return false; 89 | if (fork != that.fork) return false; 90 | if (name != null ? !name.equals(that.name) : that.name != null) return false; 91 | if (description != null ? !description.equals(that.description) : that.description != null) 92 | return false; 93 | if (language != null ? !language.equals(that.language) : that.language != null) 94 | return false; 95 | if (homepage != null ? !homepage.equals(that.homepage) : that.homepage != null) 96 | return false; 97 | return !(owner != null ? !owner.equals(that.owner) : that.owner != null); 98 | 99 | } 100 | 101 | @Override 102 | public int hashCode() { 103 | int result = (int) (id ^ (id >>> 32)); 104 | result = 31 * result + (name != null ? name.hashCode() : 0); 105 | result = 31 * result + (description != null ? description.hashCode() : 0); 106 | result = 31 * result + forks; 107 | result = 31 * result + watchers; 108 | result = 31 * result + stars; 109 | result = 31 * result + (language != null ? language.hashCode() : 0); 110 | result = 31 * result + (homepage != null ? homepage.hashCode() : 0); 111 | result = 31 * result + (owner != null ? owner.hashCode() : 0); 112 | result = 31 * result + (fork ? 1 : 0); 113 | return result; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app-mvvm/src/main/java/uk/ivanc/archimvvm/model/User.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm.model; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | public class User implements Parcelable { 9 | public long id; 10 | public String name; 11 | public String url; 12 | public String email; 13 | public String login; 14 | public String location; 15 | @SerializedName("avatar_url") 16 | public String avatarUrl; 17 | 18 | public User() { 19 | } 20 | 21 | public boolean hasEmail() { 22 | return email != null && !email.isEmpty(); 23 | } 24 | 25 | public boolean hasLocation() { 26 | return location != null && !location.isEmpty(); 27 | } 28 | 29 | @Override 30 | public int describeContents() { 31 | return 0; 32 | } 33 | 34 | @Override 35 | public void writeToParcel(Parcel dest, int flags) { 36 | dest.writeLong(this.id); 37 | dest.writeString(this.name); 38 | dest.writeString(this.url); 39 | dest.writeString(this.email); 40 | dest.writeString(this.login); 41 | dest.writeString(this.location); 42 | dest.writeString(this.avatarUrl); 43 | } 44 | 45 | protected User(Parcel in) { 46 | this.id = in.readLong(); 47 | this.name = in.readString(); 48 | this.url = in.readString(); 49 | this.email = in.readString(); 50 | this.login = in.readString(); 51 | this.location = in.readString(); 52 | this.avatarUrl = in.readString(); 53 | } 54 | 55 | public static final Creator CREATOR = new Creator() { 56 | public User createFromParcel(Parcel source) { 57 | return new User(source); 58 | } 59 | 60 | public User[] newArray(int size) { 61 | return new User[size]; 62 | } 63 | }; 64 | 65 | @Override 66 | public boolean equals(Object o) { 67 | if (this == o) return true; 68 | if (o == null || getClass() != o.getClass()) return false; 69 | 70 | User user = (User) o; 71 | 72 | if (id != user.id) return false; 73 | if (name != null ? !name.equals(user.name) : user.name != null) return false; 74 | if (url != null ? !url.equals(user.url) : user.url != null) return false; 75 | if (email != null ? !email.equals(user.email) : user.email != null) return false; 76 | if (login != null ? !login.equals(user.login) : user.login != null) return false; 77 | if (location != null ? !location.equals(user.location) : user.location != null) 78 | return false; 79 | return !(avatarUrl != null ? !avatarUrl.equals(user.avatarUrl) : user.avatarUrl != null); 80 | 81 | } 82 | 83 | @Override 84 | public int hashCode() { 85 | int result = (int) (id ^ (id >>> 32)); 86 | result = 31 * result + (name != null ? name.hashCode() : 0); 87 | result = 31 * result + (url != null ? url.hashCode() : 0); 88 | result = 31 * result + (email != null ? email.hashCode() : 0); 89 | result = 31 * result + (login != null ? login.hashCode() : 0); 90 | result = 31 * result + (location != null ? location.hashCode() : 0); 91 | result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0); 92 | return result; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app-mvvm/src/main/java/uk/ivanc/archimvvm/view/MainActivity.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm.view; 2 | 3 | import android.content.Context; 4 | import android.databinding.DataBindingUtil; 5 | import android.os.Bundle; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.support.v7.widget.LinearLayoutManager; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.view.inputmethod.InputMethodManager; 10 | 11 | import java.util.List; 12 | 13 | import uk.ivanc.archimvvm.R; 14 | import uk.ivanc.archimvvm.RepositoryAdapter; 15 | import uk.ivanc.archimvvm.databinding.MainActivityBinding; 16 | import uk.ivanc.archimvvm.model.Repository; 17 | import uk.ivanc.archimvvm.viewmodel.MainViewModel; 18 | 19 | public class MainActivity extends AppCompatActivity implements MainViewModel.DataListener { 20 | 21 | private MainActivityBinding binding; 22 | private MainViewModel mainViewModel; 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | binding = DataBindingUtil.setContentView(this, R.layout.main_activity); 28 | mainViewModel = new MainViewModel(this, this); 29 | binding.setViewModel(mainViewModel); 30 | setSupportActionBar(binding.toolbar); 31 | setupRecyclerView(binding.reposRecyclerView); 32 | } 33 | 34 | @Override 35 | protected void onDestroy() { 36 | super.onDestroy(); 37 | mainViewModel.destroy(); 38 | } 39 | 40 | @Override 41 | public void onRepositoriesChanged(List repositories) { 42 | RepositoryAdapter adapter = 43 | (RepositoryAdapter) binding.reposRecyclerView.getAdapter(); 44 | adapter.setRepositories(repositories); 45 | adapter.notifyDataSetChanged(); 46 | hideSoftKeyboard(); 47 | } 48 | 49 | private void setupRecyclerView(RecyclerView recyclerView) { 50 | RepositoryAdapter adapter = new RepositoryAdapter(); 51 | recyclerView.setAdapter(adapter); 52 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 53 | } 54 | 55 | private void hideSoftKeyboard() { 56 | InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); 57 | imm.hideSoftInputFromWindow(binding.editTextUsername.getWindowToken(), 0); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /app-mvvm/src/main/java/uk/ivanc/archimvvm/view/RepositoryActivity.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm.view; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.databinding.DataBindingUtil; 6 | import android.os.Bundle; 7 | import android.support.v7.app.ActionBar; 8 | import android.support.v7.app.AppCompatActivity; 9 | 10 | import uk.ivanc.archimvvm.R; 11 | import uk.ivanc.archimvvm.databinding.RepositoryActivityBinding; 12 | import uk.ivanc.archimvvm.model.Repository; 13 | import uk.ivanc.archimvvm.viewmodel.RepositoryViewModel; 14 | 15 | public class RepositoryActivity extends AppCompatActivity { 16 | 17 | private static final String EXTRA_REPOSITORY = "EXTRA_REPOSITORY"; 18 | 19 | private RepositoryActivityBinding binding; 20 | private RepositoryViewModel repositoryViewModel; 21 | 22 | public static Intent newIntent(Context context, Repository repository) { 23 | Intent intent = new Intent(context, RepositoryActivity.class); 24 | intent.putExtra(EXTRA_REPOSITORY, repository); 25 | return intent; 26 | } 27 | 28 | @Override 29 | protected void onCreate(Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | binding = DataBindingUtil.setContentView(this, R.layout.repository_activity); 32 | setSupportActionBar(binding.toolbar); 33 | ActionBar actionBar = getSupportActionBar(); 34 | if (actionBar != null) { 35 | actionBar.setDisplayHomeAsUpEnabled(true); 36 | } 37 | 38 | Repository repository = getIntent().getParcelableExtra(EXTRA_REPOSITORY); 39 | repositoryViewModel = new RepositoryViewModel(this, repository); 40 | binding.setViewModel(repositoryViewModel); 41 | 42 | //Currently there is no way of setting an activity title using data binding 43 | setTitle(repository.name); 44 | } 45 | 46 | @Override 47 | protected void onDestroy() { 48 | super.onDestroy(); 49 | repositoryViewModel.destroy(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app-mvvm/src/main/java/uk/ivanc/archimvvm/viewmodel/ItemRepoViewModel.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm.viewmodel; 2 | 3 | import android.content.Context; 4 | import android.databinding.BaseObservable; 5 | import android.view.View; 6 | 7 | import uk.ivanc.archimvvm.R; 8 | import uk.ivanc.archimvvm.model.Repository; 9 | import uk.ivanc.archimvvm.view.RepositoryActivity; 10 | 11 | /** 12 | * View model for each item in the repositories RecyclerView 13 | */ 14 | public class ItemRepoViewModel extends BaseObservable implements ViewModel { 15 | 16 | private Repository repository; 17 | private Context context; 18 | 19 | public ItemRepoViewModel(Context context, Repository repository) { 20 | this.repository = repository; 21 | this.context = context; 22 | } 23 | 24 | public String getName() { 25 | return repository.name; 26 | } 27 | 28 | public String getDescription() { 29 | return repository.description; 30 | } 31 | 32 | public String getStars() { 33 | return context.getString(R.string.text_stars, repository.stars); 34 | } 35 | 36 | public String getWatchers() { 37 | return context.getString(R.string.text_watchers, repository.watchers); 38 | } 39 | 40 | public String getForks() { 41 | return context.getString(R.string.text_forks, repository.forks); 42 | } 43 | 44 | public void onItemClick(View view) { 45 | context.startActivity(RepositoryActivity.newIntent(context, repository)); 46 | } 47 | 48 | // Allows recycling ItemRepoViewModels within the recyclerview adapter 49 | public void setRepository(Repository repository) { 50 | this.repository = repository; 51 | notifyChange(); 52 | } 53 | 54 | @Override 55 | public void destroy() { 56 | //In this case destroy doesn't need to do anything because there is not async calls 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /app-mvvm/src/main/java/uk/ivanc/archimvvm/viewmodel/MainViewModel.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm.viewmodel; 2 | 3 | import android.content.Context; 4 | import android.databinding.ObservableField; 5 | import android.databinding.ObservableInt; 6 | import android.text.Editable; 7 | import android.text.TextWatcher; 8 | import android.util.Log; 9 | import android.view.KeyEvent; 10 | import android.view.View; 11 | import android.view.inputmethod.EditorInfo; 12 | import android.widget.TextView; 13 | 14 | import java.util.List; 15 | 16 | import retrofit2.adapter.rxjava.HttpException; 17 | import rx.Subscriber; 18 | import rx.Subscription; 19 | import rx.android.schedulers.AndroidSchedulers; 20 | import uk.ivanc.archimvvm.ArchiApplication; 21 | import uk.ivanc.archimvvm.R; 22 | import uk.ivanc.archimvvm.model.GithubService; 23 | import uk.ivanc.archimvvm.model.Repository; 24 | 25 | /** 26 | * View model for the MainActivity 27 | */ 28 | public class MainViewModel implements ViewModel { 29 | 30 | private static final String TAG = "MainViewModel"; 31 | 32 | public ObservableInt infoMessageVisibility; 33 | public ObservableInt progressVisibility; 34 | public ObservableInt recyclerViewVisibility; 35 | public ObservableInt searchButtonVisibility; 36 | public ObservableField infoMessage; 37 | 38 | private Context context; 39 | private Subscription subscription; 40 | private List repositories; 41 | private DataListener dataListener; 42 | private String editTextUsernameValue; 43 | 44 | public MainViewModel(Context context, DataListener dataListener) { 45 | this.context = context; 46 | this.dataListener = dataListener; 47 | infoMessageVisibility = new ObservableInt(View.VISIBLE); 48 | progressVisibility = new ObservableInt(View.INVISIBLE); 49 | recyclerViewVisibility = new ObservableInt(View.INVISIBLE); 50 | searchButtonVisibility = new ObservableInt(View.GONE); 51 | infoMessage = new ObservableField<>(context.getString(R.string.default_info_message)); 52 | } 53 | 54 | public void setDataListener(DataListener dataListener) { 55 | this.dataListener = dataListener; 56 | } 57 | 58 | @Override 59 | public void destroy() { 60 | if (subscription != null && !subscription.isUnsubscribed()) subscription.unsubscribe(); 61 | subscription = null; 62 | context = null; 63 | dataListener = null; 64 | } 65 | 66 | public boolean onSearchAction(TextView view, int actionId, KeyEvent event) { 67 | if (actionId == EditorInfo.IME_ACTION_SEARCH) { 68 | String username = view.getText().toString(); 69 | if (username.length() > 0) loadGithubRepos(username); 70 | return true; 71 | } 72 | return false; 73 | } 74 | 75 | public void onClickSearch(View view) { 76 | loadGithubRepos(editTextUsernameValue); 77 | } 78 | 79 | public TextWatcher getUsernameEditTextWatcher() { 80 | return new TextWatcher() { 81 | @Override 82 | public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { 83 | 84 | } 85 | 86 | @Override 87 | public void onTextChanged(CharSequence charSequence, int start, int before, int count) { 88 | editTextUsernameValue = charSequence.toString(); 89 | searchButtonVisibility.set(charSequence.length() > 0 ? View.VISIBLE : View.GONE); 90 | } 91 | 92 | @Override 93 | public void afterTextChanged(Editable editable) { 94 | 95 | } 96 | }; 97 | } 98 | 99 | private void loadGithubRepos(String username) { 100 | progressVisibility.set(View.VISIBLE); 101 | recyclerViewVisibility.set(View.INVISIBLE); 102 | infoMessageVisibility.set(View.INVISIBLE); 103 | if (subscription != null && !subscription.isUnsubscribed()) subscription.unsubscribe(); 104 | ArchiApplication application = ArchiApplication.get(context); 105 | GithubService githubService = application.getGithubService(); 106 | subscription = githubService.publicRepositories(username) 107 | .observeOn(AndroidSchedulers.mainThread()) 108 | .subscribeOn(application.defaultSubscribeScheduler()) 109 | .subscribe(new Subscriber>() { 110 | @Override 111 | public void onCompleted() { 112 | if (dataListener != null) dataListener.onRepositoriesChanged(repositories); 113 | progressVisibility.set(View.INVISIBLE); 114 | if (!repositories.isEmpty()) { 115 | recyclerViewVisibility.set(View.VISIBLE); 116 | } else { 117 | infoMessage.set(context.getString(R.string.text_empty_repos)); 118 | infoMessageVisibility.set(View.VISIBLE); 119 | } 120 | } 121 | 122 | @Override 123 | public void onError(Throwable error) { 124 | Log.e(TAG, "Error loading GitHub repos ", error); 125 | progressVisibility.set(View.INVISIBLE); 126 | if (isHttp404(error)) { 127 | infoMessage.set(context.getString(R.string.error_username_not_found)); 128 | } else { 129 | infoMessage.set(context.getString(R.string.error_loading_repos)); 130 | } 131 | infoMessageVisibility.set(View.VISIBLE); 132 | } 133 | 134 | @Override 135 | public void onNext(List repositories) { 136 | Log.i(TAG, "Repos loaded " + repositories); 137 | MainViewModel.this.repositories = repositories; 138 | } 139 | }); 140 | } 141 | 142 | private static boolean isHttp404(Throwable error) { 143 | return error instanceof HttpException && ((HttpException) error).code() == 404; 144 | } 145 | 146 | public interface DataListener { 147 | void onRepositoriesChanged(List repositories); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /app-mvvm/src/main/java/uk/ivanc/archimvvm/viewmodel/RepositoryViewModel.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm.viewmodel; 2 | 3 | import android.content.Context; 4 | import android.databinding.BindingAdapter; 5 | import android.databinding.ObservableField; 6 | import android.databinding.ObservableInt; 7 | import android.util.Log; 8 | import android.view.View; 9 | import android.widget.ImageView; 10 | 11 | import com.squareup.picasso.Picasso; 12 | 13 | import rx.Subscription; 14 | import rx.android.schedulers.AndroidSchedulers; 15 | import rx.functions.Action1; 16 | import uk.ivanc.archimvvm.ArchiApplication; 17 | import uk.ivanc.archimvvm.R; 18 | import uk.ivanc.archimvvm.model.GithubService; 19 | import uk.ivanc.archimvvm.model.Repository; 20 | import uk.ivanc.archimvvm.model.User; 21 | 22 | /** 23 | * ViewModel for the RepositoryActivity 24 | */ 25 | public class RepositoryViewModel implements ViewModel { 26 | 27 | private static final String TAG = "RepositoryViewModel"; 28 | 29 | private Repository repository; 30 | private Context context; 31 | private Subscription subscription; 32 | 33 | public ObservableField ownerName; 34 | public ObservableField ownerEmail; 35 | public ObservableField ownerLocation; 36 | public ObservableInt ownerEmailVisibility; 37 | public ObservableInt ownerLocationVisibility; 38 | public ObservableInt ownerLayoutVisibility; 39 | 40 | public RepositoryViewModel(Context context, final Repository repository) { 41 | this.repository = repository; 42 | this.context = context; 43 | this.ownerName = new ObservableField<>(); 44 | this.ownerEmail = new ObservableField<>(); 45 | this.ownerLocation = new ObservableField<>(); 46 | this.ownerLayoutVisibility = new ObservableInt(View.INVISIBLE); 47 | this.ownerEmailVisibility = new ObservableInt(View.VISIBLE); 48 | this.ownerLocationVisibility = new ObservableInt(View.VISIBLE); 49 | // Trigger loading the rest of the user data as soon as the view model is created. 50 | // It's odd having to trigger this from here. Cases where accessing to the data model 51 | // needs to happen because of a change in the Activity/Fragment lifecycle 52 | // (i.e. an activity created) don't work very well with this MVVM pattern. 53 | // It also makes this class more difficult to test. Hopefully a better solution will be found 54 | loadFullUser(repository.owner.url); 55 | } 56 | 57 | public String getDescription() { 58 | return repository.description; 59 | } 60 | 61 | public String getHomepage() { 62 | return repository.homepage; 63 | } 64 | 65 | public int getHomepageVisibility() { 66 | return repository.hasHomepage() ? View.VISIBLE : View.GONE; 67 | } 68 | 69 | public String getLanguage() { 70 | return context.getString(R.string.text_language, repository.language); 71 | } 72 | 73 | public int getLanguageVisibility() { 74 | return repository.hasLanguage() ? View.VISIBLE : View.GONE; 75 | } 76 | 77 | public int getForkVisibility() { 78 | return repository.isFork() ? View.VISIBLE : View.GONE; 79 | } 80 | 81 | public String getOwnerAvatarUrl() { 82 | return repository.owner.avatarUrl; 83 | } 84 | 85 | @Override 86 | public void destroy() { 87 | this.context = null; 88 | if (subscription != null && !subscription.isUnsubscribed()) subscription.unsubscribe(); 89 | } 90 | 91 | @BindingAdapter({"imageUrl"}) 92 | public static void loadImage(ImageView view, String imageUrl) { 93 | Picasso.with(view.getContext()) 94 | .load(imageUrl) 95 | .placeholder(R.drawable.placeholder) 96 | .into(view); 97 | } 98 | 99 | private void loadFullUser(String url) { 100 | ArchiApplication application = ArchiApplication.get(context); 101 | GithubService githubService = application.getGithubService(); 102 | subscription = githubService.userFromUrl(url) 103 | .observeOn(AndroidSchedulers.mainThread()) 104 | .subscribeOn(application.defaultSubscribeScheduler()) 105 | .subscribe(new Action1() { 106 | @Override 107 | public void call(User user) { 108 | Log.i(TAG, "Full user data loaded " + user); 109 | ownerName.set(user.name); 110 | ownerEmail.set(user.email); 111 | ownerLocation.set(user.location); 112 | ownerEmailVisibility.set(user.hasEmail() ? View.VISIBLE : View.GONE); 113 | ownerLocationVisibility.set(user.hasLocation() ? View.VISIBLE : View.GONE); 114 | ownerLayoutVisibility.set(View.VISIBLE); 115 | } 116 | }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app-mvvm/src/main/java/uk/ivanc/archimvvm/viewmodel/ViewModel.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm.viewmodel; 2 | 3 | /** 4 | * Interface that every ViewModel must implement 5 | */ 6 | public interface ViewModel { 7 | 8 | void destroy(); 9 | } 10 | -------------------------------------------------------------------------------- /app-mvvm/src/main/res/drawable-hdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvvm/src/main/res/drawable-hdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app-mvvm/src/main/res/drawable-mdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvvm/src/main/res/drawable-mdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app-mvvm/src/main/res/drawable-xhdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvvm/src/main/res/drawable-xhdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app-mvvm/src/main/res/drawable-xxhdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvvm/src/main/res/drawable-xxhdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app-mvvm/src/main/res/drawable-xxhdpi/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvvm/src/main/res/drawable-xxhdpi/octocat.png -------------------------------------------------------------------------------- /app-mvvm/src/main/res/drawable-xxhdpi/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvvm/src/main/res/drawable-xxhdpi/placeholder.png -------------------------------------------------------------------------------- /app-mvvm/src/main/res/drawable-xxxhdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvvm/src/main/res/drawable-xxxhdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app-mvvm/src/main/res/layout/item_repo.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 22 | 23 | 30 | 31 | 43 | 44 | 56 | 57 | 61 | 62 | 66 | 67 | 76 | 77 | 86 | 87 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /app-mvvm/src/main/res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 17 | 18 | 24 | 25 | 33 | 34 | 45 | 46 | 57 | 58 | 70 | 71 | 72 | 73 | 81 | 82 | 96 | 97 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /app-mvvm/src/main/res/layout/repository_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 18 | 19 | 25 | 26 | 32 | 33 | 40 | 41 | 53 | 54 | 55 | 56 | 65 | 66 | 75 | 76 | 86 | 87 | 96 | 97 | 102 | 103 | 111 | 112 | 117 | 118 | 126 | 127 | 135 | 136 | 146 | 147 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /app-mvvm/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvvm/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-mvvm/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvvm/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-mvvm/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvvm/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-mvvm/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app-mvvm/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-mvvm/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | #ffffff 5 | #75ffffff 6 | #e1e1e1 7 | #3F51B5 8 | #303F9F 9 | #C5CAE9 10 | #03A9F4 11 | #212121 12 | #727272 13 | #FFFFFF 14 | #cbcbcb 15 | -------------------------------------------------------------------------------- /app-mvvm/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12dp 4 | 12dp 5 | 6dp 6 | 6dp 7 | 8 | -------------------------------------------------------------------------------- /app-mvvm/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Archi MVVM 3 | 4 | Hello world! 5 | Settings 6 | %d \nStars 7 | %d \nWatchers 8 | %d \nForks 9 | Oops, something went wrong 10 | This account doesn\'t have any public repository 11 | Oops, Octocat doesn\'t know that username 12 | GitHub username 13 | Enter a GitHub username above to see its repositories 14 | This repository is a fork 15 | Language: %s 16 | 17 | -------------------------------------------------------------------------------- /app-mvvm/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app-mvvm/src/test/java/uk/ivanc/archimvvm/ItemRepoViewModelTest.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.databinding.Observable; 6 | import android.view.View; 7 | 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.robolectric.RobolectricGradleTestRunner; 12 | import org.robolectric.RuntimeEnvironment; 13 | import org.robolectric.annotation.Config; 14 | 15 | import uk.ivanc.archimvvm.model.Repository; 16 | import uk.ivanc.archimvvm.viewmodel.ItemRepoViewModel; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.mockito.Matchers.any; 20 | import static org.mockito.Matchers.anyInt; 21 | import static org.mockito.Mockito.mock; 22 | import static org.mockito.Mockito.verify; 23 | 24 | @RunWith(RobolectricGradleTestRunner.class) 25 | @Config(constants = BuildConfig.class, sdk = 21) 26 | public class ItemRepoViewModelTest { 27 | 28 | ArchiApplication application; 29 | 30 | @Before 31 | public void setUp() { 32 | application = (ArchiApplication) RuntimeEnvironment.application; 33 | } 34 | 35 | @Test 36 | public void shouldGetName() { 37 | Repository repository = new Repository(); 38 | repository.name = "ivan"; 39 | ItemRepoViewModel itemRepoViewModel = new ItemRepoViewModel(application, repository); 40 | assertEquals(repository.name, itemRepoViewModel.getName()); 41 | } 42 | 43 | @Test 44 | public void shouldGetDescription() { 45 | Repository repository = new Repository(); 46 | repository.description = "This is the description"; 47 | ItemRepoViewModel itemRepoViewModel = new ItemRepoViewModel(application, repository); 48 | assertEquals(repository.description, itemRepoViewModel.getDescription()); 49 | } 50 | 51 | @Test 52 | public void shouldGetStars() { 53 | Repository repository = new Repository(); 54 | repository.stars = 10; 55 | String expectedString = application.getString(R.string.text_stars, repository.stars); 56 | ItemRepoViewModel itemRepoViewModel = new ItemRepoViewModel(application, repository); 57 | assertEquals(expectedString, itemRepoViewModel.getStars()); 58 | } 59 | 60 | @Test 61 | public void shouldGetForks() { 62 | Repository repository = new Repository(); 63 | repository.forks = 5; 64 | String expectedString = application.getString(R.string.text_forks, repository.forks); 65 | 66 | ItemRepoViewModel itemRepoViewModel = new ItemRepoViewModel(application, repository); 67 | assertEquals(expectedString, itemRepoViewModel.getForks()); 68 | } 69 | 70 | @Test 71 | public void shouldGetWatchers() { 72 | Repository repository = new Repository(); 73 | repository.watchers = 7; 74 | String expectedString = application.getString(R.string.text_watchers, repository.watchers); 75 | 76 | ItemRepoViewModel itemRepoViewModel = new ItemRepoViewModel(application, repository); 77 | assertEquals(expectedString, itemRepoViewModel.getWatchers()); 78 | } 79 | 80 | @Test 81 | public void shouldStartActivityOnItemClick() { 82 | Repository repository = new Repository(); 83 | Context mockContext = mock(Context.class); 84 | ItemRepoViewModel itemRepoViewModel = new ItemRepoViewModel(mockContext, repository); 85 | itemRepoViewModel.onItemClick(new View(application)); 86 | verify(mockContext).startActivity(any(Intent.class)); 87 | } 88 | 89 | @Test 90 | public void shouldNotifyPropertyChangeWhenSetRepository() { 91 | Repository repository = new Repository(); 92 | ItemRepoViewModel itemRepoViewModel = new ItemRepoViewModel(application, repository); 93 | Observable.OnPropertyChangedCallback mockCallback = 94 | mock(Observable.OnPropertyChangedCallback.class); 95 | itemRepoViewModel.addOnPropertyChangedCallback(mockCallback); 96 | 97 | itemRepoViewModel.setRepository(repository); 98 | verify(mockCallback).onPropertyChanged(any(Observable.class), anyInt()); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app-mvvm/src/test/java/uk/ivanc/archimvvm/MainViewModelTest.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm; 2 | 3 | import android.view.View; 4 | import android.view.inputmethod.EditorInfo; 5 | import android.widget.TextView; 6 | 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.robolectric.RobolectricGradleTestRunner; 11 | import org.robolectric.RuntimeEnvironment; 12 | import org.robolectric.annotation.Config; 13 | 14 | import java.util.Collections; 15 | import java.util.List; 16 | 17 | import okhttp3.ResponseBody; 18 | import retrofit2.Response; 19 | import retrofit2.adapter.rxjava.HttpException; 20 | import rx.Observable; 21 | import rx.schedulers.Schedulers; 22 | import uk.ivanc.archimvvm.model.GithubService; 23 | import uk.ivanc.archimvvm.model.Repository; 24 | import uk.ivanc.archimvvm.util.MockModelFabric; 25 | import uk.ivanc.archimvvm.viewmodel.MainViewModel; 26 | 27 | import static org.junit.Assert.assertEquals; 28 | import static org.mockito.Matchers.anyListOf; 29 | import static org.mockito.Mockito.doReturn; 30 | import static org.mockito.Mockito.mock; 31 | import static org.mockito.Mockito.never; 32 | import static org.mockito.Mockito.verify; 33 | import static org.mockito.Mockito.when; 34 | 35 | @RunWith(RobolectricGradleTestRunner.class) 36 | @Config(constants = BuildConfig.class, sdk = 21) 37 | public class MainViewModelTest { 38 | 39 | GithubService githubService; 40 | ArchiApplication application; 41 | MainViewModel mainViewModel; 42 | MainViewModel.DataListener dataListener; 43 | 44 | @Before 45 | public void setUp() { 46 | githubService = mock(GithubService.class); 47 | dataListener = mock(MainViewModel.DataListener.class); 48 | application = (ArchiApplication) RuntimeEnvironment.application; 49 | // Mock the retrofit service so we don't call the API directly 50 | application.setGithubService(githubService); 51 | // Change the default subscribe schedulers so all observables 52 | // will now run on the same thread 53 | application.setDefaultSubscribeScheduler(Schedulers.immediate()); 54 | mainViewModel = new MainViewModel(application, dataListener); 55 | } 56 | 57 | 58 | @Test 59 | public void shouldSearchUsernameWithRepos() { 60 | String username = "usernameWithRepos"; 61 | TextView textView = new TextView(application); 62 | textView.setText(username); 63 | List mockRepos = MockModelFabric.newListOfRepositories(10); 64 | doReturn(rx.Observable.just(mockRepos)).when(githubService).publicRepositories(username); 65 | 66 | mainViewModel.onSearchAction(textView, EditorInfo.IME_ACTION_SEARCH, null); 67 | verify(dataListener).onRepositoriesChanged(mockRepos); 68 | assertEquals(mainViewModel.infoMessageVisibility.get(), View.INVISIBLE); 69 | assertEquals(mainViewModel.progressVisibility.get(), View.INVISIBLE); 70 | assertEquals(mainViewModel.recyclerViewVisibility.get(), View.VISIBLE); 71 | } 72 | 73 | @Test 74 | public void shouldSearchInvalidUsername() { 75 | String username = "invalidUsername"; 76 | TextView textView = new TextView(application); 77 | textView.setText(username); 78 | HttpException mockHttpException = 79 | new HttpException(Response.error(404, mock(ResponseBody.class))); 80 | when(githubService.publicRepositories(username)) 81 | .thenReturn(Observable.>error(mockHttpException)); 82 | 83 | mainViewModel.onSearchAction(textView, EditorInfo.IME_ACTION_SEARCH, null); 84 | verify(dataListener, never()).onRepositoriesChanged(anyListOf(Repository.class)); 85 | assertEquals(mainViewModel.infoMessage.get(), 86 | application.getString(R.string.error_username_not_found)); 87 | assertEquals(mainViewModel.infoMessageVisibility.get(), View.VISIBLE); 88 | assertEquals(mainViewModel.progressVisibility.get(), View.INVISIBLE); 89 | assertEquals(mainViewModel.recyclerViewVisibility.get(), View.INVISIBLE); 90 | } 91 | 92 | @Test 93 | public void shouldSearchUsernameWithNoRepos() { 94 | String username = "usernameWithoutRepos"; 95 | TextView textView = new TextView(application); 96 | textView.setText(username); 97 | when(githubService.publicRepositories(username)) 98 | .thenReturn(Observable.just(Collections.emptyList())); 99 | 100 | mainViewModel.onSearchAction(textView, EditorInfo.IME_ACTION_SEARCH, null); 101 | verify(dataListener).onRepositoriesChanged(Collections.emptyList()); 102 | assertEquals(mainViewModel.infoMessage.get(), 103 | application.getString(R.string.text_empty_repos)); 104 | assertEquals(mainViewModel.infoMessageVisibility.get(), View.VISIBLE); 105 | assertEquals(mainViewModel.progressVisibility.get(), View.INVISIBLE); 106 | assertEquals(mainViewModel.recyclerViewVisibility.get(), View.INVISIBLE); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /app-mvvm/src/test/java/uk/ivanc/archimvvm/RepositoryViewModelTest.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm; 2 | 3 | import android.view.View; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.robolectric.RobolectricGradleTestRunner; 9 | import org.robolectric.RuntimeEnvironment; 10 | import org.robolectric.annotation.Config; 11 | 12 | import rx.Observable; 13 | import rx.schedulers.Schedulers; 14 | import uk.ivanc.archimvvm.model.GithubService; 15 | import uk.ivanc.archimvvm.model.Repository; 16 | import uk.ivanc.archimvvm.model.User; 17 | import uk.ivanc.archimvvm.util.MockModelFabric; 18 | import uk.ivanc.archimvvm.viewmodel.RepositoryViewModel; 19 | 20 | import static org.junit.Assert.assertEquals; 21 | import static org.mockito.Mockito.mock; 22 | import static org.mockito.Mockito.when; 23 | 24 | @RunWith(RobolectricGradleTestRunner.class) 25 | @Config(constants = BuildConfig.class, sdk = 21) 26 | public class RepositoryViewModelTest { 27 | 28 | GithubService githubService; 29 | ArchiApplication application; 30 | Repository repository; 31 | User owner; 32 | RepositoryViewModel viewModel; 33 | 34 | @Before 35 | public void setUp() { 36 | githubService = mock(GithubService.class); 37 | application = (ArchiApplication) RuntimeEnvironment.application; 38 | // Mock the retrofit service so we don't call the API directly 39 | application.setGithubService(githubService); 40 | // Change the default subscribe schedulers so all observables 41 | // will now run on the same thread 42 | application.setDefaultSubscribeScheduler(Schedulers.immediate()); 43 | // Default behaviour is to load a mock owner when the view model is instantiated 44 | repository = MockModelFabric.newRepository("Repository"); 45 | owner = MockModelFabric.newUser("owner"); 46 | when(githubService.userFromUrl(repository.owner.url)) 47 | .thenReturn(Observable.just(owner)); 48 | viewModel = new RepositoryViewModel(application, repository); 49 | } 50 | 51 | @Test 52 | public void shouldGetDescription() { 53 | assertEquals(repository.description, viewModel.getDescription()); 54 | } 55 | 56 | @Test 57 | public void shouldGetHomepage() { 58 | assertEquals(repository.homepage, viewModel.getHomepage()); 59 | } 60 | 61 | @Test 62 | public void shouldGetLanguage() { 63 | assertEquals(application.getString(R.string.text_language, repository.language), 64 | viewModel.getLanguage()); 65 | } 66 | 67 | @Test 68 | public void shouldReturnHomepageVisibilityGone() { 69 | repository.homepage = null; 70 | assertEquals(View.GONE, viewModel.getHomepageVisibility()); 71 | } 72 | 73 | @Test 74 | public void shouldReturnLanguageVisibilityGone() { 75 | repository.language = null; 76 | assertEquals(View.GONE, viewModel.getLanguageVisibility()); 77 | } 78 | 79 | @Test 80 | public void shouldReturnForkVisibilityVisible() { 81 | repository.fork = true; 82 | assertEquals(View.VISIBLE, viewModel.getForkVisibility()); 83 | } 84 | 85 | @Test 86 | public void shouldReturnForkVisibilityGone() { 87 | repository.fork = false; 88 | assertEquals(View.GONE, viewModel.getForkVisibility()); 89 | } 90 | 91 | @Test 92 | public void shouldLoadFullOwnerOnInstantiation() { 93 | assertEquals(owner.name, viewModel.ownerName.get()); 94 | assertEquals(owner.email, viewModel.ownerEmail.get()); 95 | assertEquals(owner.location, viewModel.ownerLocation.get()); 96 | assertEquals(View.VISIBLE, viewModel.ownerEmailVisibility.get()); 97 | assertEquals(View.VISIBLE, viewModel.ownerLocationVisibility.get()); 98 | assertEquals(View.VISIBLE, viewModel.ownerLayoutVisibility.get()); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app-mvvm/src/test/java/uk/ivanc/archimvvm/util/MockModelFabric.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archimvvm.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Random; 6 | 7 | import uk.ivanc.archimvvm.model.Repository; 8 | import uk.ivanc.archimvvm.model.User; 9 | 10 | public class MockModelFabric { 11 | 12 | public static List newListOfRepositories(int numRepos) { 13 | List repositories = new ArrayList<>(numRepos); 14 | for (int i = 0; i < numRepos; i++) { 15 | repositories.add(newRepository("Repo " + i)); 16 | } 17 | return repositories; 18 | } 19 | 20 | public static Repository newRepository(String name) { 21 | Random random = new Random(); 22 | Repository repository = new Repository(); 23 | repository.name = name; 24 | repository.id = random.nextInt(10000); 25 | repository.description = "Description for " + name; 26 | repository.watchers = random.nextInt(100); 27 | repository.forks = random.nextInt(100); 28 | repository.stars = random.nextInt(100); 29 | repository.owner = newUser("User-" + name); 30 | return repository; 31 | } 32 | 33 | public static User newUser(String name) { 34 | Random random = new Random(); 35 | User user = new User(); 36 | user.id = random.nextInt(10000); 37 | user.name = name; 38 | user.email = name + "@email.com"; 39 | user.location = "Location of " + name; 40 | user.url = "http://user.com/" + name; 41 | user.avatarUrl = "http://user.com/image/" + name; 42 | return user; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion rootProject.ext.androidCompileSdkVersion 5 | buildToolsVersion rootProject.ext.androidBuildToolsVersion 6 | 7 | defaultConfig { 8 | applicationId "uk.ivanc.archi" 9 | minSdkVersion rootProject.ext.androidMinSdkVersion 10 | targetSdkVersion rootProject.ext.androidTargetSdkVersion 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | Map dependencies = rootProject.ext.dependencies; 24 | 25 | compile dependencies.appCompat 26 | compile dependencies.cardView 27 | compile dependencies.recyclerView 28 | compile dependencies.retrofit 29 | compile dependencies.retrofitConverterGson 30 | compile dependencies.retrofitAdapterRxJava 31 | compile dependencies.picasso 32 | compile dependencies.rxAndroid 33 | compile dependencies.circleImageView 34 | } 35 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/ivan/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/uk/ivanc/archi/ArchiApplication.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archi; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import rx.Scheduler; 7 | import rx.schedulers.Schedulers; 8 | import uk.ivanc.archi.model.GithubService; 9 | 10 | public class ArchiApplication extends Application { 11 | 12 | private GithubService githubService; 13 | private Scheduler defaultSubscribeScheduler; 14 | 15 | public static ArchiApplication get(Context context) { 16 | return (ArchiApplication) context.getApplicationContext(); 17 | } 18 | 19 | public GithubService getGithubService() { 20 | if (githubService == null) { 21 | githubService = GithubService.Factory.create(); 22 | } 23 | return githubService; 24 | } 25 | 26 | //For setting mocks during testing 27 | public void setGithubService(GithubService githubService) { 28 | this.githubService = githubService; 29 | } 30 | 31 | public Scheduler defaultSubscribeScheduler() { 32 | if (defaultSubscribeScheduler == null) { 33 | defaultSubscribeScheduler = Schedulers.io(); 34 | } 35 | return defaultSubscribeScheduler; 36 | } 37 | 38 | //User to change scheduler from tests 39 | public void setDefaultSubscribeScheduler(Scheduler scheduler) { 40 | this.defaultSubscribeScheduler = scheduler; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/uk/ivanc/archi/MainActivity.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archi; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.support.v7.widget.Toolbar; 9 | import android.text.Editable; 10 | import android.text.TextWatcher; 11 | import android.util.Log; 12 | import android.view.KeyEvent; 13 | import android.view.View; 14 | import android.view.inputmethod.EditorInfo; 15 | import android.view.inputmethod.InputMethodManager; 16 | import android.widget.EditText; 17 | import android.widget.ImageButton; 18 | import android.widget.ProgressBar; 19 | import android.widget.TextView; 20 | 21 | import java.util.List; 22 | 23 | import retrofit2.adapter.rxjava.HttpException; 24 | import rx.Subscriber; 25 | import rx.Subscription; 26 | import rx.android.schedulers.AndroidSchedulers; 27 | import uk.ivanc.archi.model.GithubService; 28 | import uk.ivanc.archi.model.Repository; 29 | 30 | public class MainActivity extends AppCompatActivity { 31 | 32 | private static final String TAG = "MainActivity"; 33 | 34 | private Subscription subscription; 35 | private RecyclerView reposRecycleView; 36 | private Toolbar toolbar; 37 | private EditText editTextUsername; 38 | private ProgressBar progressBar; 39 | private TextView infoTextView; 40 | private ImageButton searchButton; 41 | 42 | @Override 43 | protected void onCreate(Bundle savedInstanceState) { 44 | super.onCreate(savedInstanceState); 45 | setContentView(R.layout.activity_main); 46 | progressBar = (ProgressBar) findViewById(R.id.progress); 47 | infoTextView = (TextView) findViewById(R.id.text_info); 48 | //Set up ToolBar 49 | toolbar = (Toolbar) findViewById(R.id.toolbar); 50 | setSupportActionBar(toolbar); 51 | //Set up RecyclerView 52 | reposRecycleView = (RecyclerView) findViewById(R.id.repos_recycler_view); 53 | setupRecyclerView(reposRecycleView); 54 | // Set up search button 55 | searchButton = (ImageButton) findViewById(R.id.button_search); 56 | searchButton.setOnClickListener(new View.OnClickListener() { 57 | @Override 58 | public void onClick(View v) { 59 | loadGithubRepos(editTextUsername.getText().toString()); 60 | } 61 | }); 62 | //Set up username EditText 63 | editTextUsername = (EditText) findViewById(R.id.edit_text_username); 64 | editTextUsername.addTextChangedListener(mHideShowButtonTextWatcher); 65 | editTextUsername.setOnEditorActionListener(new TextView.OnEditorActionListener() { 66 | @Override 67 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 68 | if (actionId == EditorInfo.IME_ACTION_SEARCH) { 69 | String username = editTextUsername.getText().toString(); 70 | if (username.length() > 0) loadGithubRepos(username); 71 | return true; 72 | } 73 | return false; 74 | } 75 | }); 76 | } 77 | 78 | @Override 79 | protected void onDestroy() { 80 | super.onDestroy(); 81 | if (subscription != null) subscription.unsubscribe(); 82 | } 83 | 84 | public void loadGithubRepos(String username) { 85 | progressBar.setVisibility(View.VISIBLE); 86 | reposRecycleView.setVisibility(View.GONE); 87 | infoTextView.setVisibility(View.GONE); 88 | ArchiApplication application = ArchiApplication.get(this); 89 | GithubService githubService = application.getGithubService(); 90 | subscription = githubService.publicRepositories(username) 91 | .observeOn(AndroidSchedulers.mainThread()) 92 | .subscribeOn(application.defaultSubscribeScheduler()) 93 | .subscribe(new Subscriber>() { 94 | @Override 95 | public void onCompleted() { 96 | progressBar.setVisibility(View.GONE); 97 | if (reposRecycleView.getAdapter().getItemCount() > 0) { 98 | reposRecycleView.requestFocus(); 99 | hideSoftKeyboard(); 100 | reposRecycleView.setVisibility(View.VISIBLE); 101 | } else { 102 | infoTextView.setText(R.string.text_empty_repos); 103 | infoTextView.setVisibility(View.VISIBLE); 104 | } 105 | } 106 | 107 | @Override 108 | public void onError(Throwable error) { 109 | Log.e(TAG, "Error loading GitHub repos ", error); 110 | progressBar.setVisibility(View.GONE); 111 | if (error instanceof HttpException 112 | && ((HttpException) error).code() == 404) { 113 | infoTextView.setText(R.string.error_username_not_found); 114 | } else { 115 | infoTextView.setText(R.string.error_loading_repos); 116 | } 117 | infoTextView.setVisibility(View.VISIBLE); 118 | } 119 | 120 | @Override 121 | public void onNext(List repositories) { 122 | Log.i(TAG, "Repos loaded " + repositories); 123 | RepositoryAdapter adapter = 124 | (RepositoryAdapter) reposRecycleView.getAdapter(); 125 | adapter.setRepositories(repositories); 126 | adapter.notifyDataSetChanged(); 127 | } 128 | }); 129 | } 130 | 131 | private void setupRecyclerView(RecyclerView recyclerView) { 132 | RepositoryAdapter adapter = new RepositoryAdapter(); 133 | adapter.setCallback(new RepositoryAdapter.Callback() { 134 | @Override 135 | public void onItemClick(Repository repository) { 136 | startActivity(RepositoryActivity.newIntent(MainActivity.this, repository)); 137 | } 138 | }); 139 | recyclerView.setAdapter(adapter); 140 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 141 | } 142 | 143 | private void hideSoftKeyboard() { 144 | InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); 145 | imm.hideSoftInputFromWindow(editTextUsername.getWindowToken(), 0); 146 | } 147 | 148 | private TextWatcher mHideShowButtonTextWatcher = new TextWatcher() { 149 | @Override 150 | public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { 151 | 152 | } 153 | 154 | @Override 155 | public void onTextChanged(CharSequence charSequence, int start, int before, int count) { 156 | searchButton.setVisibility(charSequence.length() > 0 ? View.VISIBLE : View.GONE); 157 | } 158 | 159 | @Override 160 | public void afterTextChanged(Editable editable) { 161 | 162 | } 163 | }; 164 | 165 | } 166 | -------------------------------------------------------------------------------- /app/src/main/java/uk/ivanc/archi/RepositoryActivity.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archi; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v7.app.ActionBar; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.support.v7.widget.Toolbar; 9 | import android.util.Log; 10 | import android.view.View; 11 | import android.widget.ImageView; 12 | import android.widget.TextView; 13 | 14 | import com.squareup.picasso.Picasso; 15 | 16 | import rx.Subscription; 17 | import rx.android.schedulers.AndroidSchedulers; 18 | import rx.functions.Action1; 19 | import uk.ivanc.archi.model.GithubService; 20 | import uk.ivanc.archi.model.Repository; 21 | import uk.ivanc.archi.model.User; 22 | 23 | public class RepositoryActivity extends AppCompatActivity { 24 | 25 | private static final String EXTRA_REPOSITORY = "EXTRA_REPOSITORY"; 26 | private static final String TAG = "RepositoryActivity"; 27 | 28 | private Toolbar toolbar; 29 | private TextView descriptionText; 30 | private TextView homepageText; 31 | private TextView languageText; 32 | private TextView forkText; 33 | private TextView ownerNameText; 34 | private TextView ownerEmailText; 35 | private TextView ownerLocationText; 36 | private ImageView ownerImage; 37 | private View ownerLayout; 38 | 39 | private Subscription subscription; 40 | 41 | public static Intent newIntent(Context context, Repository repository) { 42 | Intent intent = new Intent(context, RepositoryActivity.class); 43 | intent.putExtra(EXTRA_REPOSITORY, repository); 44 | return intent; 45 | } 46 | 47 | @Override 48 | protected void onCreate(Bundle savedInstanceState) { 49 | super.onCreate(savedInstanceState); 50 | setContentView(R.layout.activity_repository); 51 | toolbar = (Toolbar) findViewById(R.id.toolbar); 52 | setSupportActionBar(toolbar); 53 | ActionBar actionBar = getSupportActionBar(); 54 | if (actionBar != null) { 55 | actionBar.setDisplayHomeAsUpEnabled(true); 56 | } 57 | 58 | descriptionText = (TextView) findViewById(R.id.text_repo_description); 59 | homepageText = (TextView) findViewById(R.id.text_homepage); 60 | languageText = (TextView) findViewById(R.id.text_language); 61 | forkText = (TextView) findViewById(R.id.text_fork); 62 | ownerNameText = (TextView) findViewById(R.id.text_owner_name); 63 | ownerEmailText = (TextView) findViewById(R.id.text_owner_email); 64 | ownerLocationText = (TextView) findViewById(R.id.text_owner_location); 65 | ownerImage = (ImageView) findViewById(R.id.image_owner); 66 | ownerLayout = findViewById(R.id.layout_owner); 67 | 68 | Repository repository = getIntent().getParcelableExtra(EXTRA_REPOSITORY); 69 | bindRepositoryData(repository); 70 | loadFullUser(repository.owner.url); 71 | } 72 | 73 | @Override 74 | protected void onDestroy() { 75 | super.onDestroy(); 76 | if (subscription != null) subscription.unsubscribe(); 77 | } 78 | 79 | private void bindRepositoryData(final Repository repository) { 80 | setTitle(repository.name); 81 | descriptionText.setText(repository.description); 82 | homepageText.setText(repository.homepage); 83 | homepageText.setVisibility(repository.hasHomepage() ? View.VISIBLE : View.GONE); 84 | languageText.setText(getString(R.string.text_language, repository.language)); 85 | languageText.setVisibility(repository.hasLanguage() ? View.VISIBLE : View.GONE); 86 | forkText.setVisibility(repository.isFork() ? View.VISIBLE : View.GONE); 87 | //Preload image for user because we already have it before loading the full user 88 | Picasso.with(this) 89 | .load(repository.owner.avatarUrl) 90 | .placeholder(R.drawable.placeholder) 91 | .into(ownerImage); 92 | } 93 | 94 | private void bindOwnerData(final User owner) { 95 | ownerNameText.setText(owner.name); 96 | ownerEmailText.setText(owner.email); 97 | ownerEmailText.setVisibility(owner.hasEmail() ? View.VISIBLE : View.GONE); 98 | ownerLocationText.setText(owner.location); 99 | ownerLocationText.setVisibility(owner.hasLocation() ? View.VISIBLE : View.GONE); 100 | } 101 | 102 | 103 | private void loadFullUser(String url) { 104 | ArchiApplication application = ArchiApplication.get(this); 105 | GithubService githubService = application.getGithubService(); 106 | subscription = githubService.userFromUrl(url) 107 | .observeOn(AndroidSchedulers.mainThread()) 108 | .subscribeOn(application.defaultSubscribeScheduler()) 109 | .subscribe(new Action1() { 110 | @Override 111 | public void call(User user) { 112 | Log.i(TAG, "Full user data loaded " + user); 113 | bindOwnerData(user); 114 | ownerLayout.setVisibility(View.VISIBLE); 115 | } 116 | }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/uk/ivanc/archi/RepositoryAdapter.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archi; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.TextView; 9 | 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | import uk.ivanc.archi.model.Repository; 14 | 15 | public class RepositoryAdapter extends RecyclerView.Adapter { 16 | 17 | private List repositories; 18 | private Callback callback; 19 | 20 | public RepositoryAdapter() { 21 | this.repositories = Collections.emptyList(); 22 | } 23 | 24 | public RepositoryAdapter(List repositories) { 25 | this.repositories = repositories; 26 | } 27 | 28 | public void setRepositories(List repositories) { 29 | this.repositories = repositories; 30 | } 31 | 32 | public void setCallback(Callback callback) { 33 | this.callback = callback; 34 | } 35 | 36 | @Override 37 | public RepositoryViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 38 | final View itemView = LayoutInflater.from(parent.getContext()) 39 | .inflate(R.layout.item_repo, parent, false); 40 | final RepositoryViewHolder viewHolder = new RepositoryViewHolder(itemView); 41 | viewHolder.contentLayout.setOnClickListener(new View.OnClickListener() { 42 | @Override 43 | public void onClick(View v) { 44 | if (callback != null) { 45 | callback.onItemClick(viewHolder.repository); 46 | } 47 | } 48 | }); 49 | return viewHolder; 50 | } 51 | 52 | @Override 53 | public void onBindViewHolder(RepositoryViewHolder holder, int position) { 54 | Repository repository = repositories.get(position); 55 | Context context = holder.titleTextView.getContext(); 56 | holder.repository = repository; 57 | holder.titleTextView.setText(repository.name); 58 | holder.descriptionTextView.setText(repository.description); 59 | holder.watchersTextView.setText( 60 | context.getResources().getString(R.string.text_watchers, repository.watchers)); 61 | holder.starsTextView.setText( 62 | context.getResources().getString(R.string.text_stars, repository.stars)); 63 | holder.forksTextView.setText( 64 | context.getResources().getString(R.string.text_forks, repository.forks)); 65 | } 66 | 67 | @Override 68 | public int getItemCount() { 69 | return repositories.size(); 70 | } 71 | 72 | public static class RepositoryViewHolder extends RecyclerView.ViewHolder { 73 | public View contentLayout; 74 | public TextView titleTextView; 75 | public TextView descriptionTextView; 76 | public TextView watchersTextView; 77 | public TextView starsTextView; 78 | public TextView forksTextView; 79 | public Repository repository; 80 | 81 | public RepositoryViewHolder(View itemView) { 82 | super(itemView); 83 | contentLayout = itemView.findViewById(R.id.layout_content); 84 | titleTextView = (TextView) itemView.findViewById(R.id.text_repo_title); 85 | descriptionTextView = (TextView) itemView.findViewById(R.id.text_repo_description); 86 | watchersTextView = (TextView) itemView.findViewById(R.id.text_watchers); 87 | starsTextView = (TextView) itemView.findViewById(R.id.text_stars); 88 | forksTextView = (TextView) itemView.findViewById(R.id.text_forks); 89 | } 90 | } 91 | 92 | public interface Callback { 93 | void onItemClick(Repository repository); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/uk/ivanc/archi/model/GithubService.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archi.model; 2 | 3 | import java.util.List; 4 | 5 | import retrofit2.Retrofit; 6 | import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory; 7 | import retrofit2.converter.gson.GsonConverterFactory; 8 | import retrofit2.http.GET; 9 | import retrofit2.http.Path; 10 | import retrofit2.http.Url; 11 | import rx.Observable; 12 | 13 | public interface GithubService { 14 | 15 | @GET("users/{username}/repos") 16 | Observable> publicRepositories(@Path("username") String username); 17 | 18 | @GET 19 | Observable userFromUrl(@Url String userUrl); 20 | 21 | 22 | class Factory { 23 | public static GithubService create() { 24 | Retrofit retrofit = new Retrofit.Builder() 25 | .baseUrl("https://api.github.com/") 26 | .addConverterFactory(GsonConverterFactory.create()) 27 | .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) 28 | .build(); 29 | return retrofit.create(GithubService.class); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/uk/ivanc/archi/model/Repository.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archi.model; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | public class Repository implements Parcelable { 9 | public long id; 10 | public String name; 11 | public String description; 12 | public int forks; 13 | public int watchers; 14 | @SerializedName("stargazers_count") 15 | public int stars; 16 | public String language; 17 | public String homepage; 18 | public User owner; 19 | public boolean fork; 20 | 21 | public Repository() { 22 | } 23 | 24 | public boolean hasHomepage() { 25 | return homepage != null && !homepage.isEmpty(); 26 | } 27 | 28 | public boolean hasLanguage() { 29 | return language != null && !language.isEmpty(); 30 | } 31 | 32 | public boolean isFork() { 33 | return fork; 34 | } 35 | 36 | @Override 37 | public int describeContents() { 38 | return 0; 39 | } 40 | 41 | @Override 42 | public void writeToParcel(Parcel dest, int flags) { 43 | dest.writeLong(this.id); 44 | dest.writeString(this.name); 45 | dest.writeString(this.description); 46 | dest.writeInt(this.forks); 47 | dest.writeInt(this.watchers); 48 | dest.writeInt(this.stars); 49 | dest.writeString(this.language); 50 | dest.writeString(this.homepage); 51 | dest.writeParcelable(this.owner, 0); 52 | dest.writeByte(fork ? (byte) 1 : (byte) 0); 53 | } 54 | 55 | protected Repository(Parcel in) { 56 | this.id = in.readLong(); 57 | this.name = in.readString(); 58 | this.description = in.readString(); 59 | this.forks = in.readInt(); 60 | this.watchers = in.readInt(); 61 | this.stars = in.readInt(); 62 | this.language = in.readString(); 63 | this.homepage = in.readString(); 64 | this.owner = in.readParcelable(User.class.getClassLoader()); 65 | this.fork = in.readByte() != 0; 66 | } 67 | 68 | public static final Creator CREATOR = new Creator() { 69 | public Repository createFromParcel(Parcel source) { 70 | return new Repository(source); 71 | } 72 | 73 | public Repository[] newArray(int size) { 74 | return new Repository[size]; 75 | } 76 | }; 77 | 78 | @Override 79 | public boolean equals(Object o) { 80 | if (this == o) return true; 81 | if (o == null || getClass() != o.getClass()) return false; 82 | 83 | Repository that = (Repository) o; 84 | 85 | if (id != that.id) return false; 86 | if (forks != that.forks) return false; 87 | if (watchers != that.watchers) return false; 88 | if (stars != that.stars) return false; 89 | if (fork != that.fork) return false; 90 | if (name != null ? !name.equals(that.name) : that.name != null) return false; 91 | if (description != null ? !description.equals(that.description) : that.description != null) 92 | return false; 93 | if (language != null ? !language.equals(that.language) : that.language != null) 94 | return false; 95 | if (homepage != null ? !homepage.equals(that.homepage) : that.homepage != null) 96 | return false; 97 | return !(owner != null ? !owner.equals(that.owner) : that.owner != null); 98 | 99 | } 100 | 101 | @Override 102 | public int hashCode() { 103 | int result = (int) (id ^ (id >>> 32)); 104 | result = 31 * result + (name != null ? name.hashCode() : 0); 105 | result = 31 * result + (description != null ? description.hashCode() : 0); 106 | result = 31 * result + forks; 107 | result = 31 * result + watchers; 108 | result = 31 * result + stars; 109 | result = 31 * result + (language != null ? language.hashCode() : 0); 110 | result = 31 * result + (homepage != null ? homepage.hashCode() : 0); 111 | result = 31 * result + (owner != null ? owner.hashCode() : 0); 112 | result = 31 * result + (fork ? 1 : 0); 113 | return result; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/src/main/java/uk/ivanc/archi/model/User.java: -------------------------------------------------------------------------------- 1 | package uk.ivanc.archi.model; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | public class User implements Parcelable { 9 | public long id; 10 | public String name; 11 | public String url; 12 | public String email; 13 | public String login; 14 | public String location; 15 | @SerializedName("avatar_url") 16 | public String avatarUrl; 17 | 18 | public User() { 19 | } 20 | 21 | public boolean hasEmail() { 22 | return email != null && !email.isEmpty(); 23 | } 24 | 25 | public boolean hasLocation() { 26 | return location != null && !location.isEmpty(); 27 | } 28 | 29 | @Override 30 | public int describeContents() { 31 | return 0; 32 | } 33 | 34 | @Override 35 | public void writeToParcel(Parcel dest, int flags) { 36 | dest.writeLong(this.id); 37 | dest.writeString(this.name); 38 | dest.writeString(this.url); 39 | dest.writeString(this.email); 40 | dest.writeString(this.login); 41 | dest.writeString(this.location); 42 | dest.writeString(this.avatarUrl); 43 | } 44 | 45 | protected User(Parcel in) { 46 | this.id = in.readLong(); 47 | this.name = in.readString(); 48 | this.url = in.readString(); 49 | this.email = in.readString(); 50 | this.login = in.readString(); 51 | this.location = in.readString(); 52 | this.avatarUrl = in.readString(); 53 | } 54 | 55 | public static final Creator CREATOR = new Creator() { 56 | public User createFromParcel(Parcel source) { 57 | return new User(source); 58 | } 59 | 60 | public User[] newArray(int size) { 61 | return new User[size]; 62 | } 63 | }; 64 | 65 | @Override 66 | public boolean equals(Object o) { 67 | if (this == o) return true; 68 | if (o == null || getClass() != o.getClass()) return false; 69 | 70 | User user = (User) o; 71 | 72 | if (id != user.id) return false; 73 | if (name != null ? !name.equals(user.name) : user.name != null) return false; 74 | if (url != null ? !url.equals(user.url) : user.url != null) return false; 75 | if (email != null ? !email.equals(user.email) : user.email != null) return false; 76 | if (login != null ? !login.equals(user.login) : user.login != null) return false; 77 | if (location != null ? !location.equals(user.location) : user.location != null) 78 | return false; 79 | return !(avatarUrl != null ? !avatarUrl.equals(user.avatarUrl) : user.avatarUrl != null); 80 | 81 | } 82 | 83 | @Override 84 | public int hashCode() { 85 | int result = (int) (id ^ (id >>> 32)); 86 | result = 31 * result + (name != null ? name.hashCode() : 0); 87 | result = 31 * result + (url != null ? url.hashCode() : 0); 88 | result = 31 * result + (email != null ? email.hashCode() : 0); 89 | result = 31 * result + (login != null ? login.hashCode() : 0); 90 | result = 31 * result + (location != null ? location.hashCode() : 0); 91 | result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0); 92 | return result; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app/src/main/res/drawable-hdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app/src/main/res/drawable-mdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app/src/main/res/drawable-xhdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app/src/main/res/drawable-xxhdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app/src/main/res/drawable-xxhdpi/octocat.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app/src/main/res/drawable-xxhdpi/placeholder.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_search_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app/src/main/res/drawable-xxxhdpi/ic_search_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | 24 | 25 | 36 | 37 | 47 | 48 | 58 | 59 | 60 | 61 | 69 | 70 | 83 | 84 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_repository.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 23 | 24 | 31 | 32 | 43 | 44 | 45 | 46 | 55 | 56 | 63 | 64 | 72 | 73 | 81 | 82 | 87 | 88 | 96 | 97 | 101 | 102 | 110 | 111 | 118 | 119 | 127 | 128 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_repo.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 20 | 21 | 32 | 33 | 44 | 45 | 49 | 50 | 54 | 55 | 63 | 64 | 72 | 73 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | #ffffff 5 | #75ffffff 6 | #e1e1e1 7 | #3F51B5 8 | #303F9F 9 | #C5CAE9 10 | #03A9F4 11 | #212121 12 | #727272 13 | #FFFFFF 14 | #cbcbcb 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12dp 4 | 12dp 5 | 6dp 6 | 6dp 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Archi 3 | 4 | Hello world! 5 | Settings 6 | %d \nStars 7 | %d \nWatchers 8 | %d \nForks 9 | Oops, something went wrong 10 | This account doesn\'t have any public repository 11 | Oops, Octocat doesn\'t know that username 12 | GitHub username 13 | Enter a GitHub username above to see its repositories 14 | This repository is a fork 15 | Language: %s 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply from: 'dependencies.gradle' 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.3' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | jcenter() 15 | } 16 | } 17 | 18 | ext { 19 | androidCompileSdkVersion = 25 20 | androidBuildToolsVersion = '25.0.2' 21 | androidMinSdkVersion = 16 22 | androidTargetSdkVersion = 25 23 | } 24 | -------------------------------------------------------------------------------- /dependencies.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | supportLibraryVersion = '25.1.1' 3 | retrofitVersion = '2.1.0' 4 | 5 | dependencies = [ 6 | appCompat: "com.android.support:appcompat-v7:$supportLibraryVersion", 7 | cardView: "com.android.support:cardview-v7:$supportLibraryVersion", 8 | recyclerView: "com.android.support:recyclerview-v7:$supportLibraryVersion", 9 | retrofit: "com.squareup.retrofit2:retrofit:$retrofitVersion", 10 | retrofitConverterGson: "com.squareup.retrofit2:converter-gson:$retrofitVersion", 11 | retrofitAdapterRxJava: "com.squareup.retrofit2:adapter-rxjava:$retrofitVersion", 12 | picasso: 'com.squareup.picasso:picasso:2.5.2', 13 | rxAndroid: 'io.reactivex:rxandroid:1.2.1', 14 | circleImageView: 'de.hdodenhof:circleimageview:1.3.0', 15 | jUnit: 'junit:junit:4.12', 16 | mockito: 'org.mockito:mockito-core:1.10.19', 17 | robolectric: 'org.robolectric:robolectric:3.0' 18 | ] 19 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Feb 08 10:20:43 GMT 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /images/archi-screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivacf/archi/30f1c690bf2b60c904fb3c968884b113780d935a/images/archi-screenshots.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':app-mvp', ':app-mvvm' 2 | --------------------------------------------------------------------------------