diff --git a/GithubClient/.gitignore b/GithubClient/.gitignore new file mode 100644 index 0000000..7468296 --- /dev/null +++ b/GithubClient/.gitignore @@ -0,0 +1,183 @@ + +# Created by https://www.gitignore.io/api/java,android,androidstudio + +### Android ### +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# Intellij +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/dictionaries +.idea/libraries + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +### Android Patch ### +gen-external-apklibs + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files + +# Files for the ART/Dalvik VM + +# Java class files + +# Generated files + +# Gradle files +.gradle + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse + +# Log Files + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +*.ipr +*~ +*.swp + +# Android Patch + +# External native build folder generated in Android Studio 2.2 and later + +# NDK +obj/ + +# IntelliJ IDEA +*.iws +/out/ + +# User-specific configurations +.idea/libraries/ +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# OS-specific files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) +hs_err_pid* + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +### Java ### +# Compiled class file + +# Log file + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) + +# Package Files # +*.jar +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml + +# End of https://www.gitignore.io/api/java,android,androidstudio \ No newline at end of file diff --git a/GithubClient/.idea/assetWizardSettings.xml b/GithubClient/.idea/assetWizardSettings.xml new file mode 100644 index 0000000..bcef304 --- /dev/null +++ b/GithubClient/.idea/assetWizardSettings.xml @@ -0,0 +1,151 @@ + + + + + + \ No newline at end of file diff --git a/GithubClient/.idea/caches/build_file_checksums.ser b/GithubClient/.idea/caches/build_file_checksums.ser new file mode 100644 index 0000000..d2ed8f9 Binary files /dev/null and b/GithubClient/.idea/caches/build_file_checksums.ser differ diff --git a/GithubClient/.idea/caches/gradle_models.ser b/GithubClient/.idea/caches/gradle_models.ser new file mode 100644 index 0000000..baec371 Binary files /dev/null and b/GithubClient/.idea/caches/gradle_models.ser differ diff --git a/GithubClient/.idea/codeStyles/Project.xml b/GithubClient/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..6d799a0 --- /dev/null +++ b/GithubClient/.idea/codeStyles/Project.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GithubClient/.idea/dbnavigator.xml b/GithubClient/.idea/dbnavigator.xml new file mode 100644 index 0000000..4bea32f --- /dev/null +++ b/GithubClient/.idea/dbnavigator.xml @@ -0,0 +1,456 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GithubClient/.idea/runConfigurations.xml b/GithubClient/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/GithubClient/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/GithubClient/README.md b/GithubClient/README.md new file mode 100644 index 0000000..9b86911 --- /dev/null +++ b/GithubClient/README.md @@ -0,0 +1,13 @@ +# Github Client +![](https://i.imgur.com/mRyVTEd.jpg) | ![](https://i.imgur.com/MCpVsnU.jpg) | ![](https://i.imgur.com/Pykwohl.jpg) | +:-----------------------------------:|:-------------------------------------:|:---------------------------------------: + +This repository contains my Github client app which connects to public Github REST API and implements MVP architecture using Dagger2, Retrofit and RxJava. I provided unit and UI tests for presenter and view classes. + +I changed the approach to dependency injection after reading an article about [Activities Subcomponents Multibinding](https://medium.com/azimolabs/activities-subcomponents-multibinding-in-dagger-2-85d6053d6a95). + +#### Contained app packages: + - **data:** data models and Github API manager + - **di:** provided dependencies + - **ui:** view and presenter classes with contract interfaces + - **utils:** utility-helper classes diff --git a/GithubClient/app/.gitignore b/GithubClient/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/GithubClient/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/GithubClient/app/build.gradle b/GithubClient/app/build.gradle new file mode 100644 index 0000000..b42cfce --- /dev/null +++ b/GithubClient/app/build.gradle @@ -0,0 +1,71 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 28 + + defaultConfig { + applicationId "com.kurekwioletta.githubclient" + minSdkVersion 24 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "com.kurekwioletta.githubclient.TestRunner" + } + + buildTypes { + debug { + testCoverageEnabled true + buildConfigField "String", "API_ENDPOINT", API_ENDPOINT + } + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + incremental = false + } + + testOptions.unitTests.includeAndroidResources = true +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "com.android.support:appcompat-v7:$rootProject.androidSupportVersion" + implementation "com.android.support:design:$rootProject.androidSupportVersion" + implementation "com.android.support.constraint:constraint-layout:$rootProject.androidSupportConstraintLayoutVersion" + + implementation "org.apache.commons:commons-lang3:$rootProject.commonsLangVersion" + implementation "commons-io:commons-io:$rootProject.commonsIOVersion" + + implementation "com.jakewharton:butterknife:$rootProject.butterknifeVersion" + annotationProcessor "com.jakewharton:butterknife-compiler:$rootProject.butterknifeVersion" + + implementation "io.reactivex.rxjava2:rxandroid:$rootProject.rxandroidVersion" + implementation "io.reactivex.rxjava2:rxjava:$rootProject.rxjavaVersion" + + implementation "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion" + implementation "com.squareup.retrofit2:adapter-rxjava2:$rootProject.retrofitVersion" + implementation "com.squareup.retrofit2:converter-gson:$rootProject.retrofitConverterGsonVersion" + + implementation "com.google.dagger:dagger-android:$rootProject.dagger2Version" + annotationProcessor "com.google.dagger:dagger-android-processor:$rootProject.dagger2Version" + annotationProcessor "com.google.dagger:dagger-compiler:$rootProject.dagger2Version" + + testImplementation "junit:junit:$rootProject.junitVersion" + testImplementation "androidx.test:core:$rootProject.testCoreVersion" + testImplementation "org.mockito:mockito-core:$rootProject.mockitoCoreVersion" + testImplementation "org.mockito:mockito-inline:$rootProject.mockitoInlineVersion" + testImplementation "org.robolectric:robolectric:$rootProject.robolectricVersion" + + androidTestImplementation "junit:junit:$rootProject.junitVersion" + androidTestImplementation "androidx.test:core:$rootProject.testCoreVersion" + androidTestImplementation "org.mockito:mockito-core:$rootProject.mockitoCoreVersion" + androidTestImplementation "androidx.test.espresso:espresso-intents:$rootProject.espressoVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$rootProject.espressoVersion" + androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$rootProject.dexmakerMockitoVersion" +} diff --git a/GithubClient/app/proguard-rules.pro b/GithubClient/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/GithubClient/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/TestApp.java b/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/TestApp.java new file mode 100644 index 0000000..232c551 --- /dev/null +++ b/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/TestApp.java @@ -0,0 +1,18 @@ +package com.kurekwioletta.githubclient; + +import android.app.Activity; + +import com.kurekwioletta.githubclient.di.activity.ActivityComponentBuilder; + +import java.util.HashMap; +import java.util.Map; + +public class TestApp extends GithubClientApp { + + public void putActivityComponentBuilder(ActivityComponentBuilder builder, Class cls) { + Map, ActivityComponentBuilder> activityComponentBuilders = new HashMap<>(mActivityComponentBuilders); + activityComponentBuilders.put(cls, builder); + mActivityComponentBuilders = activityComponentBuilders; + } + +} diff --git a/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/TestRunner.java b/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/TestRunner.java new file mode 100644 index 0000000..cd5ecf6 --- /dev/null +++ b/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/TestRunner.java @@ -0,0 +1,16 @@ +package com.kurekwioletta.githubclient; + +import android.app.Application; +import android.content.Context; + +import androidx.test.runner.AndroidJUnitRunner; + +public class TestRunner extends AndroidJUnitRunner { + + @Override + public Application newApplication(ClassLoader cl, String className, Context context) throws + IllegalAccessException, ClassNotFoundException, InstantiationException { + return super.newApplication(cl, TestApp.class.getName(), context); + } + +} diff --git a/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/ui/main/MainActivityTest.java b/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/ui/main/MainActivityTest.java new file mode 100644 index 0000000..b624435 --- /dev/null +++ b/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/ui/main/MainActivityTest.java @@ -0,0 +1,245 @@ +package com.kurekwioletta.githubclient.ui.main; + +import android.content.Intent; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.espresso.intent.rule.IntentsTestRule; +import androidx.test.runner.AndroidJUnit4; + +import com.kurekwioletta.githubclient.R; +import com.kurekwioletta.githubclient.TestApp; +import com.kurekwioletta.githubclient.data.GithubApiManager; +import com.kurekwioletta.githubclient.di.activity.components.MainActivityComponent; +import com.kurekwioletta.githubclient.di.activity.modules.MainActivityModule; +import com.kurekwioletta.githubclient.ui.repositories_list.RepositoriesListActivity; +import com.kurekwioletta.githubclient.utils.TestConstants; +import com.kurekwioletta.githubclient.utils.Validator; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import io.reactivex.Observable; +import io.reactivex.disposables.CompositeDisposable; +import okhttp3.MediaType; +import okhttp3.ResponseBody; +import retrofit2.Response; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.pressBack; +import static androidx.test.espresso.action.ViewActions.replaceText; +import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.intent.Intents.intended; +import static androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent; +import static androidx.test.espresso.matcher.RootMatchers.isDialog; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(AndroidJUnit4.class) +public class MainActivityTest { + + private static final int RESPONSE_STATUS_CODE_SUCCESS = 200; + private static final int RESPONSE_STATUS_CODE_NOT_FOUND = 404; + private static final int RESPONSE_STATUS_CODE_UNKNOWN = 520; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Rule + public IntentsTestRule mMainActivityRule = + new IntentsTestRule<>(MainActivity.class, true, false); + + @Mock + MainActivityComponent.Builder mMockedBuilder; + + @Mock + GithubApiManager mMockedGithubApiManager; + + @Mock + Validator mMockedValidator; + + private MainActivityComponent mMainActivityComponent = new MainActivityComponent() { + @Override + public void injectMembers(MainActivity mainActivity) { + mainActivity.mainPresenter = new MainPresenter<>( + mMockedGithubApiManager, + new CompositeDisposable(), + mMockedValidator); + } + }; + + @Before + public void setUp() { + when(mMockedBuilder.build()).thenReturn(mMainActivityComponent); + when(mMockedBuilder.activityModule(any(MainActivityModule.class))) + .thenReturn(mMockedBuilder); + + TestApp app = ApplicationProvider.getApplicationContext(); + app.putActivityComponentBuilder(mMockedBuilder, MainActivity.class); + + mMainActivityRule.launchActivity(new Intent()); + } + + @Test + public void when_userExists_openRepositoriesListActivity() { + + // arrange - response success user exists + when(mMockedGithubApiManager.getUserResponse(TestConstants.VALID_USERNAME)) + .thenReturn(Observable.just(getSuccessResponse())); + + when(mMockedValidator.isUsernameValid(TestConstants.VALID_USERNAME)) + .thenReturn(true); + + // act + onView(withId(R.id.et_main_username)).perform(replaceText(TestConstants.VALID_USERNAME)); + onView(withId(R.id.btn_main_load_repositories)).perform(click()); + + // assert + onView(withId(R.id.et_main_username)).check(doesNotExist()); + intended(hasComponent(RepositoriesListActivity.class.getName())); + } + + @Test + public void when_usernameIsInvalid_showToastWithInvalidUsernameMessage() { + + // arrange - invalid username + String expectedMessage = getApplicationContext() + .getResources() + .getString(R.string.MSG_INVALID_USERNAME); + + when(mMockedValidator.isUsernameValid(TestConstants.INVALID_USERNAME)) + .thenReturn(false); + + // act + onView(withId(R.id.et_main_username)).perform(replaceText(TestConstants.INVALID_USERNAME)); + onView(withId(R.id.btn_main_load_repositories)).perform(click()); + + // assert + onView(withText(expectedMessage)) + .inRoot(isDialog()) + .check(matches(isDisplayed())); + + onView(withText(expectedMessage)).perform(pressBack()); + onView(withId(R.id.et_main_username)).check(matches(isDisplayed())); + } + + @Test + public void when_userWasNotFound_showToastWithUserWasNotFoundMessage() { + + // arrange - response error user was not found + String expectedMessage = getApplicationContext() + .getResources() + .getString(R.string.ERR_NOT_FOUND); + + when(mMockedGithubApiManager.getUserResponse(TestConstants.VALID_USERNAME)) + .thenReturn(Observable.just(getErrorResponseNotFound())); + + when(mMockedValidator.isUsernameValid(TestConstants.VALID_USERNAME)) + .thenReturn(true); + + // act + onView(withId(R.id.et_main_username)).perform(replaceText(TestConstants.VALID_USERNAME)); + onView(withId(R.id.btn_main_load_repositories)).perform(click()); + + // assert + onView(withText(expectedMessage)) + .inRoot(isDialog()) + .check(matches(isDisplayed())); + + onView(withText(expectedMessage)).perform(pressBack()); + onView(withId(R.id.et_main_username)).check(matches(isDisplayed())); + } + + + @Test + public void when_unknownErrorHasOccurred_showUnknownErrorMessage() { + + // arrange - response error unknown + String expectedMessage = getApplicationContext() + .getResources() + .getString(R.string.ERR_UNKNOWN); + + when(mMockedGithubApiManager.getUserResponse(TestConstants.VALID_USERNAME)) + .thenReturn(Observable.just(getErrorResponseUnknown())); + + when(mMockedValidator.isUsernameValid(TestConstants.VALID_USERNAME)) + .thenReturn(true); + + // act + onView(withId(R.id.et_main_username)).perform(replaceText(TestConstants.VALID_USERNAME)); + onView(withId(R.id.btn_main_load_repositories)).perform(click()); + + // assert + onView(withText(expectedMessage)) + .inRoot(isDialog()) + .check(matches(isDisplayed())); + + onView(withText(expectedMessage)).perform(pressBack()); + onView(withId(R.id.et_main_username)).check(matches(isDisplayed())); + } + + @Test + public void when_networkErrorHasOccurred_showNetworkErrorMessage() { + + // arrange - network error + String expectedMessage = getApplicationContext() + .getResources() + .getString(R.string.ERR_NETWORK); + + when(mMockedGithubApiManager.getUserResponse(TestConstants.VALID_USERNAME)) + .thenReturn(Observable.error(new RuntimeException())); + + when(mMockedValidator.isUsernameValid(TestConstants.VALID_USERNAME)) + .thenReturn(true); + + // act + onView(withId(R.id.et_main_username)).perform(replaceText(TestConstants.VALID_USERNAME)); + onView(withId(R.id.btn_main_load_repositories)).perform(click()); + + // assert + onView(withText(expectedMessage)) + .inRoot(isDialog()) + .check(matches(isDisplayed())); + + onView(withText(expectedMessage)).perform(pressBack()); + onView(withId(R.id.et_main_username)).check(matches(isDisplayed())); + } + + @After + public void tearDown() { + mMainActivityRule.finishActivity(); + } + + private Response getSuccessResponse() { + return Response.success(RESPONSE_STATUS_CODE_SUCCESS, null); + } + + private Response getErrorResponseNotFound() { + return Response.error( + RESPONSE_STATUS_CODE_NOT_FOUND, + ResponseBody.create( + MediaType.parse("application/json"), + "not found" + )); + } + + private Response getErrorResponseUnknown() { + return Response.error( + RESPONSE_STATUS_CODE_UNKNOWN, + ResponseBody.create( + MediaType.parse("application/json"), + "unknown" + )); + } +} diff --git a/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/ui/repositories_list/RepositoriesListActivityTest.java b/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/ui/repositories_list/RepositoriesListActivityTest.java new file mode 100644 index 0000000..bf01e26 --- /dev/null +++ b/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/ui/repositories_list/RepositoriesListActivityTest.java @@ -0,0 +1,178 @@ +package com.kurekwioletta.githubclient.ui.repositories_list; + +import android.content.Intent; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.espresso.intent.rule.IntentsTestRule; +import androidx.test.runner.AndroidJUnit4; + +import com.google.gson.GsonBuilder; +import com.kurekwioletta.githubclient.R; +import com.kurekwioletta.githubclient.TestApp; +import com.kurekwioletta.githubclient.data.GithubApiManager; +import com.kurekwioletta.githubclient.data.model.Repository; +import com.kurekwioletta.githubclient.di.activity.components.RepositoriesListActivityComponent; +import com.kurekwioletta.githubclient.di.activity.modules.RepositoriesListActivityModule; +import com.kurekwioletta.githubclient.ui.repositories_list.adapter.RepositoriesListAdapter; +import com.kurekwioletta.githubclient.ui.repository_details.RepositoryDetailsActivity; +import com.kurekwioletta.githubclient.utils.TestConstants; + +import org.apache.commons.io.IOUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import io.reactivex.Observable; +import io.reactivex.disposables.CompositeDisposable; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.pressBack; +import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.intent.Intents.intended; +import static androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent; +import static androidx.test.espresso.matcher.RootMatchers.isDialog; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.kurekwioletta.githubclient.utils.CustomAssertions.hasItemCount; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(AndroidJUnit4.class) +public class RepositoriesListActivityTest { + + private static final String EXTRA_USERNAME = "com.kurekwioletta.githubclient.EXTRA_USERNAME"; + + private List mRepositoriesList; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Rule + public IntentsTestRule mRepositoriesListActivityRule = + new IntentsTestRule<>(RepositoriesListActivity.class, true, false); + + @Mock + RepositoriesListActivityComponent.Builder mMockedBuilder; + + @Mock + GithubApiManager mMockedGithubApiManager; + + private RepositoriesListActivityComponent mRepositoriesListActivityComponent = + new RepositoriesListActivityComponent() { + @Override + public void injectMembers(RepositoriesListActivity repositoriesListActivity) { + repositoriesListActivity.repositoriesListPresenter = new RepositoriesListPresenter<>( + mMockedGithubApiManager, + new CompositeDisposable()); + + repositoriesListActivity.repositoriesListAdapter = new RepositoriesListAdapter( + mRepositoriesListActivityRule.getActivity()); + } + }; + + @Before + public void setUp() throws IOException { + mRepositoriesList = getRepositoriesList(); + + when(mMockedBuilder.build()).thenReturn(mRepositoriesListActivityComponent); + when(mMockedBuilder.activityModule(any(RepositoriesListActivityModule.class))) + .thenReturn(mMockedBuilder); + + TestApp app = ApplicationProvider.getApplicationContext(); + app.putActivityComponentBuilder(mMockedBuilder, RepositoriesListActivity.class); + } + + @Test + public void when_repositoriesListWasLoadedSuccessfully_updateRepositoriesList() { + + // arrange - successfully loaded repositories + when(mMockedGithubApiManager.getUsersRepositoriesList(TestConstants.VALID_USERNAME)) + .thenReturn(Observable.just(mRepositoriesList)); + + // act + mRepositoriesListActivityRule.launchActivity( + getIntentWithValidUsername()); + + // assert + onView(withId(R.id.rv_repositories_list)).check(matches(isDisplayed())); + onView(withId(R.id.rv_repositories_list)).check(hasItemCount(mRepositoriesList.size())); + } + + @Test + public void when_networkErrorHasOccurred_showNetworkErrorMessage() { + + // arrange - network error + String expectedMessage = getApplicationContext() + .getResources() + .getString(R.string.ERR_NETWORK); + + when(mMockedGithubApiManager.getUsersRepositoriesList(TestConstants.VALID_USERNAME)) + .thenReturn(Observable.error(new RuntimeException())); + + // act + mRepositoriesListActivityRule.launchActivity( + getIntentWithValidUsername()); + + // assert + onView(withText(expectedMessage)) + .inRoot(isDialog()) + .check(matches(isDisplayed())); + + onView(withText(expectedMessage)).perform(pressBack()); + onView(withId(R.id.rv_repositories_list)).check(matches(isDisplayed())); + } + + @Test + public void when_repositoryWasClicked_startRepositoryDetailsActivity() { + + // arrange - successfully loaded repository + when(mMockedGithubApiManager.getUsersRepositoriesList(TestConstants.VALID_USERNAME)) + .thenReturn(Observable.just(mRepositoriesList)); + + mRepositoriesListActivityRule.launchActivity( + getIntentWithValidUsername()); + + // act - clicked first repository + mRepositoriesListActivityRule + .getActivity() + .onRepositoryClicked(mRepositoriesList.get(0)); + + // assert + onView(withId(R.id.rv_repositories_list)).check(doesNotExist()); + intended(hasComponent(RepositoryDetailsActivity.class.getName())); + } + + @After + public void tearDown() { + mRepositoriesListActivityRule.finishActivity(); + } + + private Intent getIntentWithValidUsername() { + return new Intent().putExtra(EXTRA_USERNAME, TestConstants.VALID_USERNAME); + } + + private List getRepositoriesList() throws IOException { + GsonBuilder gsonBuilder = new GsonBuilder(); + InputStream inputStream = + Objects.requireNonNull(getClass() + .getClassLoader()) + .getResourceAsStream("repositoriesList.json"); + + return Arrays.asList(gsonBuilder.create() + .fromJson(IOUtils.toString(inputStream, "UTF-8"), Repository[].class)); + } +} diff --git a/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/utils/CustomAssertions.java b/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/utils/CustomAssertions.java new file mode 100644 index 0000000..77726e0 --- /dev/null +++ b/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/utils/CustomAssertions.java @@ -0,0 +1,48 @@ +package com.kurekwioletta.githubclient.utils; + +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.espresso.ViewAssertion; + +import org.hamcrest.CoreMatchers; + +import java.util.Objects; + +import static androidx.test.espresso.matcher.ViewMatchers.assertThat; + +public class CustomAssertions { + + public static ViewAssertion hasItemCount(int count) { + return new RecyclerViewItemCountAssertion(count); + } + + static class RecyclerViewItemCountAssertion implements ViewAssertion { + + private int mCount; + + RecyclerViewItemCountAssertion(int count) { + mCount = count; + } + + @Override + public void check(View view, NoMatchingViewException noViewFoundException) { + if (noViewFoundException != null) { + throw noViewFoundException; + } + + if (!(view instanceof RecyclerView)) { + throw new IllegalStateException("The asserted view is not RecyclerView"); + } + + if (((RecyclerView) view).getAdapter() == null) { + throw new IllegalStateException("No adapter is assigned to RecyclerView"); + } + + assertThat("RecyclerView item count", + Objects.requireNonNull(((RecyclerView) view).getAdapter()).getItemCount(), + CoreMatchers.equalTo(mCount)); + } + } +} diff --git a/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/utils/TestConstants.java b/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/utils/TestConstants.java new file mode 100644 index 0000000..4ceca08 --- /dev/null +++ b/GithubClient/app/src/androidTest/java/com/kurekwioletta/githubclient/utils/TestConstants.java @@ -0,0 +1,13 @@ +package com.kurekwioletta.githubclient.utils; + +public class TestConstants { + + // username constants + public static final String VALID_USERNAME = "KurekWioletta"; + public static final String INVALID_USERNAME = "/a"; + + private TestConstants() { + // This class is not publicly instantiable + } + +} \ No newline at end of file diff --git a/GithubClient/app/src/androidTest/resources/repositoriesList.json b/GithubClient/app/src/androidTest/resources/repositoriesList.json new file mode 100644 index 0000000..f22e8fa --- /dev/null +++ b/GithubClient/app/src/androidTest/resources/repositoriesList.json @@ -0,0 +1,472 @@ +[ + { + "id": 174545606, + "node_id": "MDEwOlJlcG9zaXRvcnkxNzQ1NDU2MDY=", + "name": "AI-PSO", + "full_name": "KurekWioletta/AI-PSO", + "private": false, + "owner": { + "login": "KurekWioletta", + "id": 37117523, + "node_id": "MDQ6VXNlcjM3MTE3NTIz", + "avatar_url": "https://avatars3.githubusercontent.com/u/37117523?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/KurekWioletta", + "html_url": "https://github.com/KurekWioletta", + "followers_url": "https://api.github.com/users/KurekWioletta/followers", + "following_url": "https://api.github.com/users/KurekWioletta/following{/other_user}", + "gists_url": "https://api.github.com/users/KurekWioletta/gists{/gist_id}", + "starred_url": "https://api.github.com/users/KurekWioletta/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/KurekWioletta/subscriptions", + "organizations_url": "https://api.github.com/users/KurekWioletta/orgs", + "repos_url": "https://api.github.com/users/KurekWioletta/repos", + "events_url": "https://api.github.com/users/KurekWioletta/events{/privacy}", + "received_events_url": "https://api.github.com/users/KurekWioletta/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/KurekWioletta/AI-PSO", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/KurekWioletta/AI-PSO", + "forks_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/forks", + "keys_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/teams", + "hooks_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/hooks", + "issue_events_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/issues/events{/number}", + "events_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/events", + "assignees_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/assignees{/user}", + "branches_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/branches{/branch}", + "tags_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/tags", + "blobs_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/statuses/{sha}", + "languages_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/languages", + "stargazers_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/stargazers", + "contributors_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/contributors", + "subscribers_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/subscribers", + "subscription_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/subscription", + "commits_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/contents/{+path}", + "compare_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/merges", + "archive_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/downloads", + "issues_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/issues{/number}", + "pulls_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/pulls{/number}", + "milestones_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/milestones{/number}", + "notifications_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/labels{/name}", + "releases_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/releases{/id}", + "deployments_url": "https://api.github.com/repos/KurekWioletta/AI-PSO/deployments", + "created_at": "2019-03-08T13:50:02Z", + "updated_at": "2019-03-08T13:59:47Z", + "pushed_at": "2019-03-08T13:59:46Z", + "git_url": "git://github.com/KurekWioletta/AI-PSO.git", + "ssh_url": "git@github.com:KurekWioletta/AI-PSO.git", + "clone_url": "https://github.com/KurekWioletta/AI-PSO.git", + "svn_url": "https://github.com/KurekWioletta/AI-PSO", + "homepage": null, + "size": 14, + "stargazers_count": 0, + "watchers_count": 0, + "language": "C++", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master" + }, + { + "id": 174609588, + "node_id": "MDEwOlJlcG9zaXRvcnkxNzQ2MDk1ODg=", + "name": "InfinityLoop", + "full_name": "KurekWioletta/InfinityLoop", + "private": false, + "owner": { + "login": "KurekWioletta", + "id": 37117523, + "node_id": "MDQ6VXNlcjM3MTE3NTIz", + "avatar_url": "https://avatars3.githubusercontent.com/u/37117523?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/KurekWioletta", + "html_url": "https://github.com/KurekWioletta", + "followers_url": "https://api.github.com/users/KurekWioletta/followers", + "following_url": "https://api.github.com/users/KurekWioletta/following{/other_user}", + "gists_url": "https://api.github.com/users/KurekWioletta/gists{/gist_id}", + "starred_url": "https://api.github.com/users/KurekWioletta/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/KurekWioletta/subscriptions", + "organizations_url": "https://api.github.com/users/KurekWioletta/orgs", + "repos_url": "https://api.github.com/users/KurekWioletta/repos", + "events_url": "https://api.github.com/users/KurekWioletta/events{/privacy}", + "received_events_url": "https://api.github.com/users/KurekWioletta/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/KurekWioletta/InfinityLoop", + "description": "App that implements MVP architecture using Dagger2, GreenDAO and RxJava.", + "fork": false, + "url": "https://api.github.com/repos/KurekWioletta/InfinityLoop", + "forks_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/forks", + "keys_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/teams", + "hooks_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/hooks", + "issue_events_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/issues/events{/number}", + "events_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/events", + "assignees_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/assignees{/user}", + "branches_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/branches{/branch}", + "tags_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/tags", + "blobs_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/statuses/{sha}", + "languages_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/languages", + "stargazers_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/stargazers", + "contributors_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/contributors", + "subscribers_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/subscribers", + "subscription_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/subscription", + "commits_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/contents/{+path}", + "compare_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/merges", + "archive_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/downloads", + "issues_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/issues{/number}", + "pulls_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/pulls{/number}", + "milestones_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/milestones{/number}", + "notifications_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/labels{/name}", + "releases_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/releases{/id}", + "deployments_url": "https://api.github.com/repos/KurekWioletta/InfinityLoop/deployments", + "created_at": "2019-03-08T21:06:38Z", + "updated_at": "2019-04-19T19:23:21Z", + "pushed_at": "2019-03-30T22:56:50Z", + "git_url": "git://github.com/KurekWioletta/InfinityLoop.git", + "ssh_url": "git@github.com:KurekWioletta/InfinityLoop.git", + "clone_url": "https://github.com/KurekWioletta/InfinityLoop.git", + "svn_url": "https://github.com/KurekWioletta/InfinityLoop", + "homepage": "", + "size": 1390, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Java", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master" + }, + { + "id": 181225904, + "node_id": "MDEwOlJlcG9zaXRvcnkxODEyMjU5MDQ=", + "name": "MobileInternshipTask", + "full_name": "KurekWioletta/MobileInternshipTask", + "private": false, + "owner": { + "login": "KurekWioletta", + "id": 37117523, + "node_id": "MDQ6VXNlcjM3MTE3NTIz", + "avatar_url": "https://avatars3.githubusercontent.com/u/37117523?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/KurekWioletta", + "html_url": "https://github.com/KurekWioletta", + "followers_url": "https://api.github.com/users/KurekWioletta/followers", + "following_url": "https://api.github.com/users/KurekWioletta/following{/other_user}", + "gists_url": "https://api.github.com/users/KurekWioletta/gists{/gist_id}", + "starred_url": "https://api.github.com/users/KurekWioletta/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/KurekWioletta/subscriptions", + "organizations_url": "https://api.github.com/users/KurekWioletta/orgs", + "repos_url": "https://api.github.com/users/KurekWioletta/repos", + "events_url": "https://api.github.com/users/KurekWioletta/events{/privacy}", + "received_events_url": "https://api.github.com/users/KurekWioletta/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/KurekWioletta/MobileInternshipTask", + "description": "Programming task for mobile internship candidates", + "fork": true, + "url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask", + "forks_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/forks", + "keys_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/teams", + "hooks_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/hooks", + "issue_events_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/issues/events{/number}", + "events_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/events", + "assignees_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/assignees{/user}", + "branches_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/branches{/branch}", + "tags_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/tags", + "blobs_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/statuses/{sha}", + "languages_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/languages", + "stargazers_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/stargazers", + "contributors_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/contributors", + "subscribers_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/subscribers", + "subscription_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/subscription", + "commits_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/contents/{+path}", + "compare_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/merges", + "archive_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/downloads", + "issues_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/issues{/number}", + "pulls_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/pulls{/number}", + "milestones_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/milestones{/number}", + "notifications_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/labels{/name}", + "releases_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/releases{/id}", + "deployments_url": "https://api.github.com/repos/KurekWioletta/MobileInternshipTask/deployments", + "created_at": "2019-04-13T20:35:14Z", + "updated_at": "2019-04-20T13:30:43Z", + "pushed_at": "2019-04-20T13:30:41Z", + "git_url": "git://github.com/KurekWioletta/MobileInternshipTask.git", + "ssh_url": "git@github.com:KurekWioletta/MobileInternshipTask.git", + "clone_url": "https://github.com/KurekWioletta/MobileInternshipTask.git", + "svn_url": "https://github.com/KurekWioletta/MobileInternshipTask", + "homepage": null, + "size": 222, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Java", + "has_issues": false, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master" + }, + { + "id": 175480608, + "node_id": "MDEwOlJlcG9zaXRvcnkxNzU0ODA2MDg=", + "name": "WFiIS-CGBasics", + "full_name": "KurekWioletta/WFiIS-CGBasics", + "private": false, + "owner": { + "login": "KurekWioletta", + "id": 37117523, + "node_id": "MDQ6VXNlcjM3MTE3NTIz", + "avatar_url": "https://avatars3.githubusercontent.com/u/37117523?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/KurekWioletta", + "html_url": "https://github.com/KurekWioletta", + "followers_url": "https://api.github.com/users/KurekWioletta/followers", + "following_url": "https://api.github.com/users/KurekWioletta/following{/other_user}", + "gists_url": "https://api.github.com/users/KurekWioletta/gists{/gist_id}", + "starred_url": "https://api.github.com/users/KurekWioletta/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/KurekWioletta/subscriptions", + "organizations_url": "https://api.github.com/users/KurekWioletta/orgs", + "repos_url": "https://api.github.com/users/KurekWioletta/repos", + "events_url": "https://api.github.com/users/KurekWioletta/events{/privacy}", + "received_events_url": "https://api.github.com/users/KurekWioletta/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/KurekWioletta/WFiIS-CGBasics", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics", + "forks_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/forks", + "keys_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/teams", + "hooks_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/hooks", + "issue_events_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/issues/events{/number}", + "events_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/events", + "assignees_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/assignees{/user}", + "branches_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/branches{/branch}", + "tags_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/tags", + "blobs_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/statuses/{sha}", + "languages_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/languages", + "stargazers_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/stargazers", + "contributors_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/contributors", + "subscribers_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/subscribers", + "subscription_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/subscription", + "commits_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/contents/{+path}", + "compare_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/merges", + "archive_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/downloads", + "issues_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/issues{/number}", + "pulls_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/pulls{/number}", + "milestones_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/milestones{/number}", + "notifications_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/labels{/name}", + "releases_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/releases{/id}", + "deployments_url": "https://api.github.com/repos/KurekWioletta/WFiIS-CGBasics/deployments", + "created_at": "2019-03-13T18:51:19Z", + "updated_at": "2019-04-03T09:43:55Z", + "pushed_at": "2019-04-03T09:43:54Z", + "git_url": "git://github.com/KurekWioletta/WFiIS-CGBasics.git", + "ssh_url": "git@github.com:KurekWioletta/WFiIS-CGBasics.git", + "clone_url": "https://github.com/KurekWioletta/WFiIS-CGBasics.git", + "svn_url": "https://github.com/KurekWioletta/WFiIS-CGBasics", + "homepage": null, + "size": 171, + "stargazers_count": 0, + "watchers_count": 0, + "language": "C", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master" + }, + { + "id": 174607052, + "node_id": "MDEwOlJlcG9zaXRvcnkxNzQ2MDcwNTI=", + "name": "WFiIS-OOP1", + "full_name": "KurekWioletta/WFiIS-OOP1", + "private": false, + "owner": { + "login": "KurekWioletta", + "id": 37117523, + "node_id": "MDQ6VXNlcjM3MTE3NTIz", + "avatar_url": "https://avatars3.githubusercontent.com/u/37117523?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/KurekWioletta", + "html_url": "https://github.com/KurekWioletta", + "followers_url": "https://api.github.com/users/KurekWioletta/followers", + "following_url": "https://api.github.com/users/KurekWioletta/following{/other_user}", + "gists_url": "https://api.github.com/users/KurekWioletta/gists{/gist_id}", + "starred_url": "https://api.github.com/users/KurekWioletta/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/KurekWioletta/subscriptions", + "organizations_url": "https://api.github.com/users/KurekWioletta/orgs", + "repos_url": "https://api.github.com/users/KurekWioletta/repos", + "events_url": "https://api.github.com/users/KurekWioletta/events{/privacy}", + "received_events_url": "https://api.github.com/users/KurekWioletta/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/KurekWioletta/WFiIS-OOP1", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1", + "forks_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/forks", + "keys_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/teams", + "hooks_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/hooks", + "issue_events_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/issues/events{/number}", + "events_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/events", + "assignees_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/assignees{/user}", + "branches_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/branches{/branch}", + "tags_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/tags", + "blobs_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/statuses/{sha}", + "languages_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/languages", + "stargazers_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/stargazers", + "contributors_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/contributors", + "subscribers_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/subscribers", + "subscription_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/subscription", + "commits_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/contents/{+path}", + "compare_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/merges", + "archive_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/downloads", + "issues_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/issues{/number}", + "pulls_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/pulls{/number}", + "milestones_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/milestones{/number}", + "notifications_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/labels{/name}", + "releases_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/releases{/id}", + "deployments_url": "https://api.github.com/repos/KurekWioletta/WFiIS-OOP1/deployments", + "created_at": "2019-03-08T20:46:45Z", + "updated_at": "2019-03-31T16:03:47Z", + "pushed_at": "2019-03-31T16:03:46Z", + "git_url": "git://github.com/KurekWioletta/WFiIS-OOP1.git", + "ssh_url": "git@github.com:KurekWioletta/WFiIS-OOP1.git", + "clone_url": "https://github.com/KurekWioletta/WFiIS-OOP1.git", + "svn_url": "https://github.com/KurekWioletta/WFiIS-OOP1", + "homepage": null, + "size": 243, + "stargazers_count": 0, + "watchers_count": 0, + "language": "C++", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master" + } +] \ No newline at end of file diff --git a/GithubClient/app/src/main/AndroidManifest.xml b/GithubClient/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..89de4ae --- /dev/null +++ b/GithubClient/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/GithubClientApp.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/GithubClientApp.java new file mode 100644 index 0000000..1860283 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/GithubClientApp.java @@ -0,0 +1,38 @@ +package com.kurekwioletta.githubclient; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; + +import com.kurekwioletta.githubclient.di.activity.ActivityComponentBuilder; +import com.kurekwioletta.githubclient.di.activity.HasActivitySubcomponentBuilders; +import com.kurekwioletta.githubclient.di.app.AppComponent; +import com.kurekwioletta.githubclient.di.app.DaggerAppComponent; + +import java.util.Map; +import java.util.Objects; + +import javax.inject.Inject; + +public class GithubClientApp extends Application implements HasActivitySubcomponentBuilders { + + @Inject + protected Map, ActivityComponentBuilder> mActivityComponentBuilders; + + @Override + public void onCreate() { + super.onCreate(); + AppComponent appComponent = DaggerAppComponent.create(); + appComponent.inject(this); + } + + @Override + public ActivityComponentBuilder getActivityComponentBuilder(Class activityClass) { + return Objects.requireNonNull(mActivityComponentBuilders.get(activityClass)); + } + + public static HasActivitySubcomponentBuilders get(Context context) { + return ((HasActivitySubcomponentBuilders) context.getApplicationContext()); + } + +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/data/AppGithubApiManager.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/data/AppGithubApiManager.java new file mode 100644 index 0000000..945c21c --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/data/AppGithubApiManager.java @@ -0,0 +1,37 @@ +package com.kurekwioletta.githubclient.data; + +import com.kurekwioletta.githubclient.data.model.Repository; +import com.kurekwioletta.githubclient.utils.rx.SchedulerProvider; + +import java.util.List; + +import javax.inject.Singleton; + +import io.reactivex.Observable; +import retrofit2.Response; + +@Singleton +public class AppGithubApiManager implements GithubApiManager { + + private final GithubApiManager mGithubApiManager; + private final SchedulerProvider mSchedulerProvider; + + public AppGithubApiManager(GithubApiManager githubApiManager, SchedulerProvider schedulerProvider) { + mGithubApiManager = githubApiManager; + mSchedulerProvider = schedulerProvider; + } + + @Override + public Observable> getUserResponse(String username) { + return mGithubApiManager.getUserResponse(username) + .subscribeOn(mSchedulerProvider.io()) + .observeOn(mSchedulerProvider.ui()); + } + + @Override + public Observable> getUsersRepositoriesList(String username) { + return mGithubApiManager.getUsersRepositoriesList(username) + .subscribeOn(mSchedulerProvider.io()) + .observeOn(mSchedulerProvider.ui()); + } +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/data/GithubApiManager.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/data/GithubApiManager.java new file mode 100644 index 0000000..963c173 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/data/GithubApiManager.java @@ -0,0 +1,20 @@ +package com.kurekwioletta.githubclient.data; + +import com.kurekwioletta.githubclient.data.model.Repository; + +import java.util.List; + +import io.reactivex.Observable; +import retrofit2.Response; +import retrofit2.http.GET; +import retrofit2.http.Path; + +public interface GithubApiManager { + + @GET("users/{username}") + Observable> getUserResponse(@Path("username") String username); + + @GET("users/{username}/repos") + Observable> getUsersRepositoriesList(@Path("username") String username); + +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/data/model/Repository.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/data/model/Repository.java new file mode 100644 index 0000000..be21740 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/data/model/Repository.java @@ -0,0 +1,120 @@ +package com.kurekwioletta.githubclient.data.model; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.google.gson.annotations.SerializedName; + +public class Repository implements Parcelable { + + @SerializedName("id") + private int id; + + @SerializedName("name") + private String name; + + @SerializedName("html_url") + private String htmlUrl; + + @SerializedName("description") + private String description; + + @SerializedName("created_at") + private String createdAt; + + @SerializedName("updated_at") + private String updatedAt; + + @SerializedName("pushed_at") + private String pushedAt; + + @SerializedName("stargazers_count") + private int stargazersCount; + + @SerializedName("watchers_count") + private int watchersCount; + + @SerializedName("forks_count") + private int forksCount; + + public Repository(Parcel in) { + id = in.readInt(); + name = in.readString(); + htmlUrl = in.readString(); + description = in.readString(); + createdAt = in.readString(); + updatedAt = in.readString(); + pushedAt = in.readString(); + stargazersCount = in.readInt(); + watchersCount = in.readInt(); + forksCount = in.readInt(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public Repository createFromParcel(Parcel in) { + return new Repository(in); + } + + @Override + public Repository[] newArray(int size) { + return new Repository[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(id); + dest.writeString(name); + dest.writeString(htmlUrl); + dest.writeString(description); + dest.writeString(createdAt); + dest.writeString(updatedAt); + dest.writeString(pushedAt); + dest.writeInt(stargazersCount); + dest.writeInt(watchersCount); + dest.writeInt(forksCount); + } + + public String getName() { + return name; + } + + public String getHtmlUrl() { + return htmlUrl; + } + + public String getDescription() { + return description; + } + + public String getCreatedAt() { + return createdAt; + } + + public String getUpdatedAt() { + return updatedAt; + } + + public String getPushedAt() { + return pushedAt; + } + + public int getStargazersCount() { + return stargazersCount; + } + + public int getWatchersCount() { + return watchersCount; + } + + public int getForksCount() { + return forksCount; + } + +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/ActivityKey.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/ActivityKey.java new file mode 100644 index 0000000..8727616 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/ActivityKey.java @@ -0,0 +1,10 @@ +package com.kurekwioletta.githubclient.di; + +import android.app.Activity; + +import dagger.MapKey; + +@MapKey +public @interface ActivityKey { + Class value(); +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/ActivityScope.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/ActivityScope.java new file mode 100644 index 0000000..dbd9ff7 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/ActivityScope.java @@ -0,0 +1,11 @@ +package com.kurekwioletta.githubclient.di; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Scope; + +@Scope +@Retention(RetentionPolicy.RUNTIME) +public @interface ActivityScope { +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/ActivityBindingModule.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/ActivityBindingModule.java new file mode 100644 index 0000000..89ba4bc --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/ActivityBindingModule.java @@ -0,0 +1,37 @@ +package com.kurekwioletta.githubclient.di.activity; + +import com.kurekwioletta.githubclient.di.ActivityKey; +import com.kurekwioletta.githubclient.di.activity.components.MainActivityComponent; +import com.kurekwioletta.githubclient.di.activity.components.RepositoriesListActivityComponent; +import com.kurekwioletta.githubclient.di.activity.components.RepositoryDetailsActivityComponent; +import com.kurekwioletta.githubclient.ui.main.MainActivity; +import com.kurekwioletta.githubclient.ui.repositories_list.RepositoriesListActivity; +import com.kurekwioletta.githubclient.ui.repository_details.RepositoryDetailsActivity; + +import dagger.Binds; +import dagger.Module; +import dagger.multibindings.IntoMap; + +@Module( + subcomponents = { + MainActivityComponent.class, + RepositoriesListActivityComponent.class, + RepositoryDetailsActivityComponent.class + }) +public abstract class ActivityBindingModule { + + @Binds + @IntoMap + @ActivityKey(MainActivity.class) + public abstract ActivityComponentBuilder mainActivityComponentBuilder(MainActivityComponent.Builder impl); + + @Binds + @IntoMap + @ActivityKey(RepositoriesListActivity.class) + public abstract ActivityComponentBuilder repositoriesListActivityComponentBuilder(RepositoriesListActivityComponent.Builder impl); + + @Binds + @IntoMap + @ActivityKey(RepositoryDetailsActivity.class) + public abstract ActivityComponentBuilder repositoryDetailsActivityComponentBuilder(RepositoryDetailsActivityComponent.Builder impl); +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/ActivityComponentBuilder.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/ActivityComponentBuilder.java new file mode 100644 index 0000000..ef434be --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/ActivityComponentBuilder.java @@ -0,0 +1,12 @@ +package com.kurekwioletta.githubclient.di.activity; + +import com.kurekwioletta.githubclient.di.activity.components.ActivityComponent; +import com.kurekwioletta.githubclient.di.activity.modules.ActivityModule; + +public interface ActivityComponentBuilder { + + ActivityComponentBuilder activityModule(M activityModule); + + C build(); + +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/HasActivitySubcomponentBuilders.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/HasActivitySubcomponentBuilders.java new file mode 100644 index 0000000..e733eef --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/HasActivitySubcomponentBuilders.java @@ -0,0 +1,9 @@ +package com.kurekwioletta.githubclient.di.activity; + +import android.app.Activity; + +public interface HasActivitySubcomponentBuilders { + + ActivityComponentBuilder getActivityComponentBuilder(Class activityClass); + +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/components/ActivityComponent.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/components/ActivityComponent.java new file mode 100644 index 0000000..b752ba6 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/components/ActivityComponent.java @@ -0,0 +1,9 @@ +package com.kurekwioletta.githubclient.di.activity.components; + +import android.app.Activity; + +import dagger.MembersInjector; + +public interface ActivityComponent extends MembersInjector { + +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/components/MainActivityComponent.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/components/MainActivityComponent.java new file mode 100644 index 0000000..bf1838f --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/components/MainActivityComponent.java @@ -0,0 +1,20 @@ +package com.kurekwioletta.githubclient.di.activity.components; + +import com.kurekwioletta.githubclient.di.ActivityScope; +import com.kurekwioletta.githubclient.di.activity.ActivityComponentBuilder; +import com.kurekwioletta.githubclient.di.activity.modules.MainActivityModule; +import com.kurekwioletta.githubclient.ui.main.MainActivity; + +import dagger.Subcomponent; + +@ActivityScope +@Subcomponent( + modules = MainActivityModule.class +) +public interface MainActivityComponent extends ActivityComponent { + + @Subcomponent.Builder + interface Builder extends ActivityComponentBuilder { + } + +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/components/RepositoriesListActivityComponent.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/components/RepositoriesListActivityComponent.java new file mode 100644 index 0000000..5251c9b --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/components/RepositoriesListActivityComponent.java @@ -0,0 +1,20 @@ +package com.kurekwioletta.githubclient.di.activity.components; + +import com.kurekwioletta.githubclient.di.ActivityScope; +import com.kurekwioletta.githubclient.di.activity.ActivityComponentBuilder; +import com.kurekwioletta.githubclient.di.activity.modules.RepositoriesListActivityModule; +import com.kurekwioletta.githubclient.ui.repositories_list.RepositoriesListActivity; + +import dagger.Subcomponent; + +@ActivityScope +@Subcomponent( + modules = RepositoriesListActivityModule.class +) +public interface RepositoriesListActivityComponent extends ActivityComponent { + + @Subcomponent.Builder + interface Builder extends ActivityComponentBuilder { + } + +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/components/RepositoryDetailsActivityComponent.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/components/RepositoryDetailsActivityComponent.java new file mode 100644 index 0000000..4463bd0 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/components/RepositoryDetailsActivityComponent.java @@ -0,0 +1,20 @@ +package com.kurekwioletta.githubclient.di.activity.components; + +import com.kurekwioletta.githubclient.di.ActivityScope; +import com.kurekwioletta.githubclient.di.activity.ActivityComponentBuilder; +import com.kurekwioletta.githubclient.di.activity.modules.RepositoryDetailsActivityModule; +import com.kurekwioletta.githubclient.ui.repository_details.RepositoryDetailsActivity; + +import dagger.Subcomponent; + +@ActivityScope +@Subcomponent( + modules = RepositoryDetailsActivityModule.class +) +public interface RepositoryDetailsActivityComponent extends ActivityComponent { + + @Subcomponent.Builder + interface Builder extends ActivityComponentBuilder { + } + +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/modules/ActivityModule.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/modules/ActivityModule.java new file mode 100644 index 0000000..2611fed --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/modules/ActivityModule.java @@ -0,0 +1,39 @@ +package com.kurekwioletta.githubclient.di.activity.modules; + +import android.content.Context; +import android.support.v7.app.AppCompatActivity; + +import com.kurekwioletta.githubclient.di.ActivityScope; + +import dagger.Module; +import dagger.Provides; +import io.reactivex.disposables.CompositeDisposable; + +@Module +public abstract class ActivityModule { + + private final A mActivity; + + ActivityModule(A activity) { + mActivity = activity; + } + + @Provides + @ActivityScope + A provideActivity() { + return mActivity; + } + + @Provides + @ActivityScope + Context provideContext() { + return mActivity; + } + + @Provides + @ActivityScope + CompositeDisposable provideCompositeDisposable() { + return new CompositeDisposable(); + } + +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/modules/MainActivityModule.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/modules/MainActivityModule.java new file mode 100644 index 0000000..6e8c224 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/modules/MainActivityModule.java @@ -0,0 +1,14 @@ +package com.kurekwioletta.githubclient.di.activity.modules; + +import com.kurekwioletta.githubclient.ui.main.MainActivity; + +import dagger.Module; + +@Module +public class MainActivityModule extends ActivityModule { + + public MainActivityModule(MainActivity activity) { + super(activity); + } + +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/modules/RepositoriesListActivityModule.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/modules/RepositoriesListActivityModule.java new file mode 100644 index 0000000..d064665 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/modules/RepositoriesListActivityModule.java @@ -0,0 +1,24 @@ +package com.kurekwioletta.githubclient.di.activity.modules; + +import android.content.Context; +import android.support.v7.widget.LinearLayoutManager; + +import com.kurekwioletta.githubclient.di.ActivityScope; +import com.kurekwioletta.githubclient.ui.repositories_list.RepositoriesListActivity; + +import dagger.Module; +import dagger.Provides; + +@Module +public class RepositoriesListActivityModule extends ActivityModule { + + public RepositoriesListActivityModule(RepositoriesListActivity activity) { + super(activity); + } + + @Provides + @ActivityScope + LinearLayoutManager provideLinearLayoutManager(Context context) { + return new LinearLayoutManager(context); + } +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/modules/RepositoryDetailsActivityModule.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/modules/RepositoryDetailsActivityModule.java new file mode 100644 index 0000000..35e61c3 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/activity/modules/RepositoryDetailsActivityModule.java @@ -0,0 +1,14 @@ +package com.kurekwioletta.githubclient.di.activity.modules; + +import com.kurekwioletta.githubclient.ui.repository_details.RepositoryDetailsActivity; + +import dagger.Module; + +@Module +public class RepositoryDetailsActivityModule extends ActivityModule { + + public RepositoryDetailsActivityModule(RepositoryDetailsActivity activity) { + super(activity); + } + +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/app/AppComponent.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/app/AppComponent.java new file mode 100644 index 0000000..2ad8393 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/app/AppComponent.java @@ -0,0 +1,21 @@ +package com.kurekwioletta.githubclient.di.app; + +import com.kurekwioletta.githubclient.GithubClientApp; +import com.kurekwioletta.githubclient.di.activity.ActivityBindingModule; + +import javax.inject.Singleton; + +import dagger.Component; + +@Singleton +@Component( + modules = { + AppModule.class, + GithubApiModule.class, + ActivityBindingModule.class + }) +public interface AppComponent { + + void inject(GithubClientApp githubClientApp); + +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/app/AppModule.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/app/AppModule.java new file mode 100644 index 0000000..0f2ab76 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/app/AppModule.java @@ -0,0 +1,13 @@ +package com.kurekwioletta.githubclient.di.app; + +import android.app.Application; + +import dagger.Module; + +@Module +public class AppModule { + + public AppModule(Application application) { + + } +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/app/GithubApiModule.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/app/GithubApiModule.java new file mode 100644 index 0000000..733417d --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/di/app/GithubApiModule.java @@ -0,0 +1,57 @@ +package com.kurekwioletta.githubclient.di.app; + +import com.kurekwioletta.githubclient.BuildConfig; +import com.kurekwioletta.githubclient.data.AppGithubApiManager; +import com.kurekwioletta.githubclient.data.GithubApiManager; +import com.kurekwioletta.githubclient.utils.rx.AppSchedulerProvider; +import com.kurekwioletta.githubclient.utils.rx.SchedulerProvider; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; + +@Module +public class GithubApiModule { + + @Provides + @Singleton + GithubApiManager provideGithubApiManager(AppGithubApiManager appGithubApiManager) { + return appGithubApiManager; + } + + @Provides + @Singleton + AppGithubApiManager provideAppGithubApiManager(Retrofit retrofit, SchedulerProvider schedulerProvider) { + GithubApiManager githubApiManager = retrofit.create(GithubApiManager.class); + return new AppGithubApiManager(githubApiManager, schedulerProvider); + } + + @Provides + @Singleton + Retrofit provideRetrofit(OkHttpClient okHttpClient) { + return new Retrofit.Builder() + .baseUrl(BuildConfig.API_ENDPOINT) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .client(okHttpClient) + .build(); + } + + @Provides + @Singleton + OkHttpClient provideOkHttpClient() { + return new OkHttpClient.Builder().build(); + } + + @Provides + @Singleton + SchedulerProvider provideSchedulerProvider() { + return new AppSchedulerProvider(); + } + +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/base/BaseActivity.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/base/BaseActivity.java new file mode 100644 index 0000000..a6ad4df --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/base/BaseActivity.java @@ -0,0 +1,52 @@ +package com.kurekwioletta.githubclient.ui.base; + +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; + +import com.kurekwioletta.githubclient.GithubClientApp; +import com.kurekwioletta.githubclient.di.activity.HasActivitySubcomponentBuilders; + +import butterknife.Unbinder; + +public abstract class BaseActivity extends AppCompatActivity implements MvpView { + + private Unbinder mUnbinder; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setUpActivityComponent(); + } + + @Override + public void showMessage(int resId) { + AlertDialog alertDialog = new AlertDialog.Builder(this).create(); + alertDialog.setMessage(getResources().getString(resId)); + alertDialog.setButton( + DialogInterface.BUTTON_NEUTRAL, + "OK", + (dialog, id) -> dialog.dismiss()); + alertDialog.show(); + } + + @Override + protected void onDestroy() { + if (mUnbinder != null) { + mUnbinder.unbind(); + } + super.onDestroy(); + } + + protected void setUpActivityComponent() { + injectMembers(GithubClientApp.get(this)); + } + + protected abstract void injectMembers(HasActivitySubcomponentBuilders hasActivitySubcomponentBuilders); + + protected void setUnBinder(Unbinder unBinder) { + mUnbinder = unBinder; + } +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/base/BasePresenter.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/base/BasePresenter.java new file mode 100644 index 0000000..9bfaf3d --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/base/BasePresenter.java @@ -0,0 +1,41 @@ +package com.kurekwioletta.githubclient.ui.base; + +import com.kurekwioletta.githubclient.data.GithubApiManager; + +import io.reactivex.disposables.CompositeDisposable; + +public class BasePresenter implements MvpPresenter { + + private final GithubApiManager mGithubApiManager; + private final CompositeDisposable mCompositeDisposable; + + private V mMvpView; + + public BasePresenter(GithubApiManager githubApiManager, CompositeDisposable compositeDisposable) { + mGithubApiManager = githubApiManager; + mCompositeDisposable = compositeDisposable; + } + + @Override + public void onAttach(V mvpView) { + mMvpView = mvpView; + } + + @Override + public void onDetach() { + mCompositeDisposable.dispose(); + mMvpView = null; + } + + protected V getMvpView() { + return mMvpView; + } + + protected GithubApiManager getGithubApiManager() { + return mGithubApiManager; + } + + protected CompositeDisposable getCompositeDisposable() { + return mCompositeDisposable; + } +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/base/MvpPresenter.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/base/MvpPresenter.java new file mode 100644 index 0000000..5f8d08c --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/base/MvpPresenter.java @@ -0,0 +1,9 @@ +package com.kurekwioletta.githubclient.ui.base; + +public interface MvpPresenter { + + void onAttach(V mvpView); + + void onDetach(); + +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/base/MvpView.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/base/MvpView.java new file mode 100644 index 0000000..f971f86 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/base/MvpView.java @@ -0,0 +1,9 @@ +package com.kurekwioletta.githubclient.ui.base; + +import android.support.annotation.StringRes; + +public interface MvpView { + + void showMessage(@StringRes int resId); + +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/main/MainActivity.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/main/MainActivity.java new file mode 100644 index 0000000..cfbf7d3 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/main/MainActivity.java @@ -0,0 +1,76 @@ +package com.kurekwioletta.githubclient.ui.main; + +import android.os.Bundle; +import android.view.View; +import android.widget.EditText; +import android.widget.ProgressBar; + +import com.kurekwioletta.githubclient.R; +import com.kurekwioletta.githubclient.di.activity.HasActivitySubcomponentBuilders; +import com.kurekwioletta.githubclient.di.activity.components.MainActivityComponent; +import com.kurekwioletta.githubclient.di.activity.modules.MainActivityModule; +import com.kurekwioletta.githubclient.ui.base.BaseActivity; +import com.kurekwioletta.githubclient.ui.repositories_list.RepositoriesListActivity; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; + +public class MainActivity extends BaseActivity implements MainContract.View { + + @Inject + MainPresenter mainPresenter; + + @BindView(R.id.et_main_username) + EditText etUsername; + + @BindView(R.id.pb_main_loading) + ProgressBar pbLoading; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + setUnBinder(ButterKnife.bind(this)); + + mainPresenter.onAttach(this); + } + + @Override + protected void injectMembers(HasActivitySubcomponentBuilders hasActivitySubcomponentBuilders) { + ((MainActivityComponent.Builder) hasActivitySubcomponentBuilders + .getActivityComponentBuilder(MainActivity.class)) + .activityModule(new MainActivityModule(this)) + .build() + .injectMembers(this); + } + + @Override + public void openRepositoriesListActivity(String username) { + startActivity(RepositoriesListActivity.getStartIntent(this, username)); + } + + @Override + public void showLoading() { + pbLoading.setVisibility(View.VISIBLE); + } + + @Override + public void hideLoading() { + pbLoading.setVisibility(View.INVISIBLE); + } + + @Override + protected void onDestroy() { + mainPresenter.onDetach(); + super.onDestroy(); + } + + @OnClick(R.id.btn_main_load_repositories) + public void onLoadRepositoriesClick(View v) { + mainPresenter.onLoadRepositoriesClick(etUsername.getText().toString()); + } +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/main/MainContract.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/main/MainContract.java new file mode 100644 index 0000000..23b385e --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/main/MainContract.java @@ -0,0 +1,20 @@ +package com.kurekwioletta.githubclient.ui.main; + +import com.kurekwioletta.githubclient.di.ActivityScope; +import com.kurekwioletta.githubclient.ui.base.MvpPresenter; +import com.kurekwioletta.githubclient.ui.base.MvpView; + +public interface MainContract { + interface View extends MvpView { + void openRepositoriesListActivity(String username); + + void showLoading(); + + void hideLoading(); + } + + @ActivityScope + interface Presenter extends MvpPresenter { + void onLoadRepositoriesClick(String username); + } +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/main/MainPresenter.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/main/MainPresenter.java new file mode 100644 index 0000000..e0e180b --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/main/MainPresenter.java @@ -0,0 +1,65 @@ +package com.kurekwioletta.githubclient.ui.main; + +import com.kurekwioletta.githubclient.R; +import com.kurekwioletta.githubclient.data.GithubApiManager; +import com.kurekwioletta.githubclient.di.ActivityScope; +import com.kurekwioletta.githubclient.ui.base.BasePresenter; +import com.kurekwioletta.githubclient.utils.Validator; +import com.kurekwioletta.githubclient.utils.rx.DisposableObserverWrapper; + +import javax.inject.Inject; + +import io.reactivex.disposables.CompositeDisposable; +import retrofit2.Response; + +@ActivityScope +public class MainPresenter extends BasePresenter implements MainContract.Presenter { + + private static final int RESPONSE_STATUS_CODE_SUCCESS = 200; + private static final int RESPONSE_STATUS_CODE_NOT_FOUND = 404; + + private Validator mValidator; + + @Inject + public MainPresenter(GithubApiManager githubApiManager, CompositeDisposable compositeDisposable, Validator validator) { + super(githubApiManager, compositeDisposable); + mValidator = validator; + } + + @Override + public void onLoadRepositoriesClick(final String username) { + if (mValidator.isUsernameValid(username)) { + getMvpView().showLoading(); + + // if user exists - open RepositoriesListActivity + getCompositeDisposable().add( + getGithubApiManager().getUserResponse(username) + .subscribeWith(new DisposableObserverWrapper>() { + @Override + public void onNext(Response response) { + getMvpView().hideLoading(); + switch (response.code()) { + case RESPONSE_STATUS_CODE_SUCCESS: + getMvpView().openRepositoriesListActivity(username); + break; + case RESPONSE_STATUS_CODE_NOT_FOUND: + getMvpView().showMessage(R.string.ERR_NOT_FOUND); + break; + default: + getMvpView().showMessage(R.string.ERR_UNKNOWN); + break; + } + } + + @Override + public void onError(Throwable e) { + getMvpView().hideLoading(); + getMvpView().showMessage(R.string.ERR_NETWORK); + } + }) + ); + } else { + getMvpView().showMessage(R.string.MSG_INVALID_USERNAME); + } + } +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/RepositoriesListActivity.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/RepositoriesListActivity.java new file mode 100644 index 0000000..4049657 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/RepositoriesListActivity.java @@ -0,0 +1,102 @@ +package com.kurekwioletta.githubclient.ui.repositories_list; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.constraint.ConstraintLayout; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import com.kurekwioletta.githubclient.R; +import com.kurekwioletta.githubclient.data.model.Repository; +import com.kurekwioletta.githubclient.di.activity.HasActivitySubcomponentBuilders; +import com.kurekwioletta.githubclient.di.activity.components.RepositoriesListActivityComponent; +import com.kurekwioletta.githubclient.di.activity.modules.RepositoriesListActivityModule; +import com.kurekwioletta.githubclient.ui.base.BaseActivity; +import com.kurekwioletta.githubclient.ui.repositories_list.adapter.RepositoriesListAdapter; +import com.kurekwioletta.githubclient.ui.repository_details.RepositoryDetailsActivity; + +import java.util.List; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class RepositoriesListActivity extends BaseActivity implements RepositoriesListContract.View { + + private static final String EXTRA_USERNAME = "com.kurekwioletta.githubclient.EXTRA_USERNAME"; + + @Inject + RepositoriesListPresenter repositoriesListPresenter; + + @Inject + RepositoriesListAdapter repositoriesListAdapter; + + @Inject + LinearLayoutManager linearLayoutManager; + + @BindView(R.id.rv_repositories_list) + RecyclerView rvRepositoriesList; + + @BindView(R.id.cl_repositories_list_loading) + ConstraintLayout clLoading; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_repositories_list); + + setUnBinder(ButterKnife.bind(this)); + + repositoriesListPresenter.onAttach(this); + setUpRepositoriesListRecyclerView(); + repositoriesListPresenter.onViewInitialized(getIntent().getStringExtra(EXTRA_USERNAME)); + } + + @Override + protected void injectMembers(HasActivitySubcomponentBuilders hasActivitySubcomponentBuilders) { + ((RepositoriesListActivityComponent.Builder) hasActivitySubcomponentBuilders + .getActivityComponentBuilder(RepositoriesListActivity.class)) + .activityModule(new RepositoriesListActivityModule(this)) + .build() + .injectMembers(this); + } + + @Override + public void updateRepositoriesListRecyclerView(List repositoriesList) { + repositoriesListAdapter.updateRepositoriesList(repositoriesList); + } + + @Override + public void showLoading() { + clLoading.setVisibility(View.VISIBLE); + } + + @Override + public void hideLoading() { + clLoading.setVisibility(View.GONE); + } + + @Override + protected void onDestroy() { + repositoriesListPresenter.onDetach(); + super.onDestroy(); + } + + public void onRepositoryClicked(Repository repository) { + startActivity(RepositoryDetailsActivity.getStartIntent(this, repository)); + } + + public static Intent getStartIntent(Context context, String username) { + Intent intent = new Intent(context, RepositoriesListActivity.class); + intent.putExtra(EXTRA_USERNAME, username); + return intent; + } + + private void setUpRepositoriesListRecyclerView() { + rvRepositoriesList.setLayoutManager(linearLayoutManager); + rvRepositoriesList.setAdapter(repositoriesListAdapter); + } +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/RepositoriesListContract.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/RepositoriesListContract.java new file mode 100644 index 0000000..f926ba0 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/RepositoriesListContract.java @@ -0,0 +1,23 @@ +package com.kurekwioletta.githubclient.ui.repositories_list; + +import com.kurekwioletta.githubclient.data.model.Repository; +import com.kurekwioletta.githubclient.di.ActivityScope; +import com.kurekwioletta.githubclient.ui.base.MvpPresenter; +import com.kurekwioletta.githubclient.ui.base.MvpView; + +import java.util.List; + +public interface RepositoriesListContract { + interface View extends MvpView { + void updateRepositoriesListRecyclerView(List repositoriesList); + + void showLoading(); + + void hideLoading(); + } + + @ActivityScope + interface Presenter extends MvpPresenter { + void onViewInitialized(String username); + } +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/RepositoriesListPresenter.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/RepositoriesListPresenter.java new file mode 100644 index 0000000..90d8ae3 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/RepositoriesListPresenter.java @@ -0,0 +1,47 @@ +package com.kurekwioletta.githubclient.ui.repositories_list; + +import com.kurekwioletta.githubclient.R; +import com.kurekwioletta.githubclient.data.GithubApiManager; +import com.kurekwioletta.githubclient.data.model.Repository; +import com.kurekwioletta.githubclient.di.ActivityScope; +import com.kurekwioletta.githubclient.ui.base.BasePresenter; +import com.kurekwioletta.githubclient.utils.rx.DisposableObserverWrapper; + +import java.util.List; + +import javax.inject.Inject; + +import io.reactivex.disposables.CompositeDisposable; + +@ActivityScope +public class RepositoriesListPresenter extends BasePresenter implements RepositoriesListContract.Presenter { + + @Inject + public RepositoriesListPresenter(GithubApiManager githubApiManager, CompositeDisposable compositeDisposable) { + super(githubApiManager, compositeDisposable); + } + + @Override + public void onViewInitialized(String username) { + getMvpView().showLoading(); + + // download users repositories list + getCompositeDisposable().add(getGithubApiManager() + .getUsersRepositoriesList(username) + .subscribeWith(new DisposableObserverWrapper>() { + @Override + public void onNext(List repositoriesList) { + getMvpView().hideLoading(); + getMvpView().updateRepositoriesListRecyclerView(repositoriesList); + } + + @Override + public void onError(Throwable e) { + getMvpView().hideLoading(); + getMvpView().showMessage(R.string.ERR_NETWORK); + } + }) + ); + } + +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/adapter/RepositoriesListAdapter.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/adapter/RepositoriesListAdapter.java new file mode 100644 index 0000000..1949477 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/adapter/RepositoriesListAdapter.java @@ -0,0 +1,65 @@ +package com.kurekwioletta.githubclient.ui.repositories_list.adapter; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.kurekwioletta.githubclient.R; +import com.kurekwioletta.githubclient.data.model.Repository; +import com.kurekwioletta.githubclient.di.ActivityScope; +import com.kurekwioletta.githubclient.ui.repositories_list.RepositoriesListActivity; +import com.kurekwioletta.githubclient.ui.repositories_list.adapter.viewholder.RepositoriesListViewHolder; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +@ActivityScope +public class RepositoriesListAdapter extends RecyclerView.Adapter { + + private final List mRepositoriesList = new ArrayList<>(); + + private Context mContext; + + @Inject + public RepositoriesListAdapter(Context context) { + mContext = context; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); + return new RepositoriesListViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + ((RepositoriesListViewHolder) holder).bindData(mRepositoriesList.get(position)); + + holder.itemView.setOnClickListener(v -> onItemClicked(holder.getLayoutPosition())); + } + + @Override + public int getItemCount() { + return mRepositoriesList.size(); + } + + @Override + public int getItemViewType(final int position) { + return R.layout.item_repository; + } + + public void updateRepositoriesList(List repositoriesList) { + mRepositoriesList.addAll(repositoriesList); + notifyDataSetChanged(); + } + + private void onItemClicked(int position) { + ((RepositoriesListActivity) mContext).onRepositoryClicked(mRepositoriesList.get(position)); + } +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/adapter/viewholder/RepositoriesListViewHolder.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/adapter/viewholder/RepositoriesListViewHolder.java new file mode 100644 index 0000000..a29bb24 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repositories_list/adapter/viewholder/RepositoriesListViewHolder.java @@ -0,0 +1,32 @@ +package com.kurekwioletta.githubclient.ui.repositories_list.adapter.viewholder; + +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.TextView; + +import com.kurekwioletta.githubclient.R; +import com.kurekwioletta.githubclient.data.model.Repository; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class RepositoriesListViewHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.tv_repository_name) + TextView tvName; + + @BindView(R.id.tv_repository_stars) + TextView tvStars; + + public RepositoriesListViewHolder(@NonNull View parent) { + super(parent); + ButterKnife.bind(this, parent); + } + + public void bindData(final Repository repository) { + tvName.setText(repository.getName()); + tvStars.setText(String.valueOf(repository.getStargazersCount())); + } + +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repository_details/RepositoryDetailsActivity.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repository_details/RepositoryDetailsActivity.java new file mode 100644 index 0000000..cae74e8 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repository_details/RepositoryDetailsActivity.java @@ -0,0 +1,103 @@ +package com.kurekwioletta.githubclient.ui.repository_details; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.TextView; + +import com.kurekwioletta.githubclient.R; +import com.kurekwioletta.githubclient.data.model.Repository; +import com.kurekwioletta.githubclient.di.activity.HasActivitySubcomponentBuilders; +import com.kurekwioletta.githubclient.di.activity.components.RepositoryDetailsActivityComponent; +import com.kurekwioletta.githubclient.di.activity.modules.RepositoryDetailsActivityModule; +import com.kurekwioletta.githubclient.ui.base.BaseActivity; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class RepositoryDetailsActivity extends BaseActivity implements RepositoryDetailsContract.View { + + private static final String EXTRA_REPOSITORY = "com.kurekwioletta.githubclient.EXTRA_REPOSITORY"; + + @Inject + RepositoryDetailsPresenter repositoryDetailsPresenter; + + @BindView(R.id.tv_repository_details_name) + TextView tvName; + + @BindView(R.id.tv_repository_details_description) + TextView tvDescription; + + @BindView(R.id.tv_repository_details_watchers) + TextView tvWatchers; + + @BindView(R.id.tv_repository_details_stars) + TextView tvStars; + + @BindView(R.id.tv_repository_details_forks) + TextView tvForks; + + @BindView(R.id.tv_repository_details_created) + TextView tvCreated; + + @BindView(R.id.tv_repository_details_updated) + TextView tvUpdated; + + @BindView(R.id.tv_repository_details_pushed) + TextView tvPushed; + + @BindView(R.id.tv_repository_details_hyperlink) + TextView tvHyperlink; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_repository_details); + + setUnBinder(ButterKnife.bind(this)); + + repositoryDetailsPresenter.onAttach(this); + repositoryDetailsPresenter.onViewInitialized(getIntent().getParcelableExtra(EXTRA_REPOSITORY)); + } + + @Override + protected void injectMembers(HasActivitySubcomponentBuilders hasActivitySubcomponentBuilders) { + ((RepositoryDetailsActivityComponent.Builder) hasActivitySubcomponentBuilders + .getActivityComponentBuilder(RepositoryDetailsActivity.class)) + .activityModule(new RepositoryDetailsActivityModule(this)) + .build() + .injectMembers(this); + } + + @Override + public void setUpRepositoryDetails(Repository repository) { + tvName.setText(repository.getName()); + if (repository.getDescription() == null) { + tvDescription.setVisibility(View.GONE); + } else { + tvDescription.setText(repository.getDescription()); + } + tvWatchers.setText(String.valueOf(repository.getWatchersCount())); + tvStars.setText(String.valueOf(repository.getStargazersCount())); + tvForks.setText(String.valueOf(repository.getForksCount())); + tvCreated.setText(repository.getCreatedAt().substring(0, 10)); // yyyy-mm-dd + tvUpdated.setText(repository.getUpdatedAt().substring(0, 10)); + tvPushed.setText(repository.getPushedAt().substring(0, 10)); + tvHyperlink.setText(repository.getHtmlUrl()); + } + + @Override + protected void onDestroy() { + repositoryDetailsPresenter.onDetach(); + super.onDestroy(); + } + + public static Intent getStartIntent(Context context, Repository repository) { + Intent intent = new Intent(context, RepositoryDetailsActivity.class); + intent.putExtra(EXTRA_REPOSITORY, repository); + return intent; + } +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repository_details/RepositoryDetailsContract.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repository_details/RepositoryDetailsContract.java new file mode 100644 index 0000000..9b2c4d7 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repository_details/RepositoryDetailsContract.java @@ -0,0 +1,17 @@ +package com.kurekwioletta.githubclient.ui.repository_details; + +import com.kurekwioletta.githubclient.data.model.Repository; +import com.kurekwioletta.githubclient.di.ActivityScope; +import com.kurekwioletta.githubclient.ui.base.MvpPresenter; +import com.kurekwioletta.githubclient.ui.base.MvpView; + +public interface RepositoryDetailsContract { + interface View extends MvpView { + void setUpRepositoryDetails(Repository repository); + } + + @ActivityScope + interface Presenter extends MvpPresenter { + void onViewInitialized(Repository repository); + } +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repository_details/RepositoryDetailsPresenter.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repository_details/RepositoryDetailsPresenter.java new file mode 100644 index 0000000..5de7aa4 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/ui/repository_details/RepositoryDetailsPresenter.java @@ -0,0 +1,24 @@ +package com.kurekwioletta.githubclient.ui.repository_details; + +import com.kurekwioletta.githubclient.data.GithubApiManager; +import com.kurekwioletta.githubclient.data.model.Repository; +import com.kurekwioletta.githubclient.di.ActivityScope; +import com.kurekwioletta.githubclient.ui.base.BasePresenter; + +import javax.inject.Inject; + +import io.reactivex.disposables.CompositeDisposable; + +@ActivityScope +public class RepositoryDetailsPresenter extends BasePresenter implements RepositoryDetailsContract.Presenter { + + @Inject + public RepositoryDetailsPresenter(GithubApiManager githubApiManager, CompositeDisposable compositeDisposable) { + super(githubApiManager, compositeDisposable); + } + + @Override + public void onViewInitialized(Repository repository) { + getMvpView().setUpRepositoryDetails(repository); + } +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/utils/Validator.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/utils/Validator.java new file mode 100644 index 0000000..5679a70 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/utils/Validator.java @@ -0,0 +1,31 @@ +package com.kurekwioletta.githubclient.utils; + +import com.kurekwioletta.githubclient.di.ActivityScope; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.inject.Inject; + +@ActivityScope +public class Validator { + + @Inject + public Validator() { + + } + + /** + * Validate username with regular expression. + * + * @param username may only contain alphanumeric characters or hyphens, + * cannot have multiple consecutive hyphens, + * cannot begin or end with a hyphen, + * max 39 characters. + */ + public boolean isUsernameValid(String username) { + Pattern pattern = Pattern.compile("[a-zA-Z0-9](-?[a-zA-Z0-9]){0,38}"); + Matcher matcher = pattern.matcher(username); + return matcher.matches(); + } +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/utils/rx/AppSchedulerProvider.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/utils/rx/AppSchedulerProvider.java new file mode 100644 index 0000000..3b2506b --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/utils/rx/AppSchedulerProvider.java @@ -0,0 +1,19 @@ +package com.kurekwioletta.githubclient.utils.rx; + +import io.reactivex.Scheduler; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +public class AppSchedulerProvider implements SchedulerProvider { + + @Override + public Scheduler ui() { + return AndroidSchedulers.mainThread(); + } + + @Override + public Scheduler io() { + return Schedulers.io(); + } + +} diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/utils/rx/DisposableObserverWrapper.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/utils/rx/DisposableObserverWrapper.java new file mode 100644 index 0000000..7167782 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/utils/rx/DisposableObserverWrapper.java @@ -0,0 +1,19 @@ +package com.kurekwioletta.githubclient.utils.rx; + +import io.reactivex.observers.DisposableObserver; + +public abstract class DisposableObserverWrapper extends DisposableObserver { + + @Override + public void onComplete() { + } + + @Override + public void onNext(T t) { + } + + @Override + public void onError(Throwable e) { + } + +} \ No newline at end of file diff --git a/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/utils/rx/SchedulerProvider.java b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/utils/rx/SchedulerProvider.java new file mode 100644 index 0000000..6159b16 --- /dev/null +++ b/GithubClient/app/src/main/java/com/kurekwioletta/githubclient/utils/rx/SchedulerProvider.java @@ -0,0 +1,11 @@ +package com.kurekwioletta.githubclient.utils.rx; + +import io.reactivex.Scheduler; + +public interface SchedulerProvider { + + Scheduler ui(); + + Scheduler io(); + +} \ No newline at end of file diff --git a/GithubClient/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/GithubClient/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..e24ab2b --- /dev/null +++ b/GithubClient/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/GithubClient/app/src/main/res/drawable/gd_main_background.xml b/GithubClient/app/src/main/res/drawable/gd_main_background.xml new file mode 100644 index 0000000..3d726dd --- /dev/null +++ b/GithubClient/app/src/main/res/drawable/gd_main_background.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/GithubClient/app/src/main/res/drawable/gd_main_button_neutral.xml b/GithubClient/app/src/main/res/drawable/gd_main_button_neutral.xml new file mode 100644 index 0000000..152c2fa --- /dev/null +++ b/GithubClient/app/src/main/res/drawable/gd_main_button_neutral.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/GithubClient/app/src/main/res/drawable/gd_main_button_pressed.xml b/GithubClient/app/src/main/res/drawable/gd_main_button_pressed.xml new file mode 100644 index 0000000..49af6e0 --- /dev/null +++ b/GithubClient/app/src/main/res/drawable/gd_main_button_pressed.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/GithubClient/app/src/main/res/drawable/ic_launcher_background.xml b/GithubClient/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..92c8e7e --- /dev/null +++ b/GithubClient/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/GithubClient/app/src/main/res/drawable/ic_launcher_foreground.xml b/GithubClient/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..c55b386 --- /dev/null +++ b/GithubClient/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/GithubClient/app/src/main/res/drawable/ic_main_github_mark.xml b/GithubClient/app/src/main/res/drawable/ic_main_github_mark.xml new file mode 100644 index 0000000..7fd3c5c --- /dev/null +++ b/GithubClient/app/src/main/res/drawable/ic_main_github_mark.xml @@ -0,0 +1,9 @@ + + + diff --git a/GithubClient/app/src/main/res/drawable/ic_repository_details_eye.xml b/GithubClient/app/src/main/res/drawable/ic_repository_details_eye.xml new file mode 100644 index 0000000..e5fd3d7 --- /dev/null +++ b/GithubClient/app/src/main/res/drawable/ic_repository_details_eye.xml @@ -0,0 +1,12 @@ + + + + diff --git a/GithubClient/app/src/main/res/drawable/ic_repository_details_fork.xml b/GithubClient/app/src/main/res/drawable/ic_repository_details_fork.xml new file mode 100644 index 0000000..233e079 --- /dev/null +++ b/GithubClient/app/src/main/res/drawable/ic_repository_details_fork.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/GithubClient/app/src/main/res/drawable/ic_repository_details_star.xml b/GithubClient/app/src/main/res/drawable/ic_repository_details_star.xml new file mode 100644 index 0000000..d431e81 --- /dev/null +++ b/GithubClient/app/src/main/res/drawable/ic_repository_details_star.xml @@ -0,0 +1,9 @@ + + + diff --git a/GithubClient/app/src/main/res/drawable/ic_repository_star.xml b/GithubClient/app/src/main/res/drawable/ic_repository_star.xml new file mode 100644 index 0000000..759f1e1 --- /dev/null +++ b/GithubClient/app/src/main/res/drawable/ic_repository_star.xml @@ -0,0 +1,9 @@ + + + diff --git a/GithubClient/app/src/main/res/drawable/sl_main_button.xml b/GithubClient/app/src/main/res/drawable/sl_main_button.xml new file mode 100644 index 0000000..3c69902 --- /dev/null +++ b/GithubClient/app/src/main/res/drawable/sl_main_button.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/GithubClient/app/src/main/res/layout/activity_main.xml b/GithubClient/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..6635aac --- /dev/null +++ b/GithubClient/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + +