Skip to content
Permalink
Browse files
Refactoring the package structure and adding a ViewModel
  • Loading branch information
florina-muntenescu committed Jul 17, 2018
1 parent 645bf45 commit a33ba90e51a4c48fe4acc7d883ed2b160e5b03b8
Showing 28 changed files with 477 additions and 109 deletions.
@@ -62,6 +62,8 @@ repositories {
}

dependencies {
api project(':bypass')

api "androidx.core:core-ktx:${versions.coreKtx}"
api "com.android.support.constraint:constraint-layout:${versions.constraintLayout}"
api "com.android.support:customtabs:${versions.supportLibrary}"
@@ -84,12 +86,13 @@ dependencies {
api "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-experimental-adapter:" +
"${versions.retrofitCoroutines}"
api "org.jsoup:jsoup:${versions.jsoup}"
api project(':bypass')

api "android.arch.lifecycle:viewmodel:${versions.lifecycle}"
api "android.arch.lifecycle:extensions:${versions.lifecycle}"

testImplementation "junit:junit:${versions.junit}"
testImplementation "org.mockito:mockito-core:${versions.mockito}"
testImplementation "com.squareup.retrofit2:retrofit-mock:${versions.retrofit}"
testImplementation "android.arch.core:core-testing:${versions.lifecycle}"

androidTestImplementation "com.android.support.test:runner:${versions.test_runner}"
androidTestImplementation "com.android.support.test:rules:${versions.test_rules}"
@@ -14,27 +14,27 @@
* limitations under the License.
*/

package io.plaidapp.designernews.login.data
package io.plaidapp.designernews.ui.login.data

import android.content.Context
import android.support.test.InstrumentationRegistry.getInstrumentation
import io.plaidapp.core.designernews.data.api.model.User
import io.plaidapp.core.designernews.login.data.DesignerNewsLoginLocalDataSource
import io.plaidapp.core.designernews.login.data.LoginLocalDataSource
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test

/**
* Tests for [DesignerNewsLoginLocalDataSource] using shared preferences from instrumentation
* Tests for [LoginLocalDataSource] using shared preferences from instrumentation
* context
*/
class DesignerNewsLoginDataSourceTest {

private var sharedPreferences = getInstrumentation().context
.getSharedPreferences("test", Context.MODE_PRIVATE)

private var dataSource = DesignerNewsLoginLocalDataSource(sharedPreferences)
private var dataSource = LoginLocalDataSource(sharedPreferences)

@After
fun tearDown() {
@@ -22,7 +22,7 @@

import io.plaidapp.core.designernews.data.api.DesignerNewsService;
import io.plaidapp.core.designernews.data.api.model.User;
import io.plaidapp.core.designernews.login.data.DesignerNewsLoginRepository;
import io.plaidapp.core.designernews.login.data.LoginRepository;
import io.plaidapp.core.util.ShortcutHelper;

/**
@@ -32,7 +32,7 @@ public class DesignerNewsPrefs {

private static volatile DesignerNewsPrefs singleton;

private DesignerNewsLoginRepository loginRepository;
private LoginRepository loginRepository;

public static DesignerNewsPrefs get(Context context) {
if (singleton == null) {
@@ -44,7 +44,7 @@ public static DesignerNewsPrefs get(Context context) {
return singleton;
}

private DesignerNewsPrefs(DesignerNewsLoginRepository loginRepository) {
private DesignerNewsPrefs(LoginRepository loginRepository) {
this.loginRepository = loginRepository;
}

@@ -28,6 +28,9 @@ import io.plaidapp.core.designernews.data.api.ClientAuthInterceptor
import io.plaidapp.core.designernews.data.api.DesignerNewsAuthTokenLocalDataSource
import io.plaidapp.core.designernews.data.api.DesignerNewsRepository
import io.plaidapp.core.designernews.data.api.DesignerNewsService
import io.plaidapp.core.designernews.login.data.LoginLocalDataSource
import io.plaidapp.core.designernews.login.data.LoginRemoteDataSource
import io.plaidapp.core.designernews.login.data.LoginRepository
import io.plaidapp.core.designernews.data.comments.CommentsRepository
import io.plaidapp.core.designernews.data.votes.DesignerNewsVotesRepository
import io.plaidapp.core.designernews.data.votes.VotesRemoteDataSource
@@ -36,9 +39,6 @@ import io.plaidapp.core.designernews.data.users.UserRemoteDataSource
import io.plaidapp.core.designernews.data.users.UserRepository
import io.plaidapp.core.designernews.domain.CommentsUseCase
import io.plaidapp.core.designernews.domain.CommentsWithRepliesUseCase
import io.plaidapp.core.designernews.login.data.DesignerNewsLoginLocalDataSource
import io.plaidapp.core.designernews.login.data.DesignerNewsLoginRemoteDataSource
import io.plaidapp.core.designernews.login.data.DesignerNewsLoginRepository
import io.plaidapp.core.loggingInterceptor
import io.plaidapp.core.provideCoroutinesContextProvider
import io.plaidapp.core.provideSharedPreferences
@@ -52,22 +52,22 @@ import retrofit2.converter.gson.GsonConverterFactory
* Once we have a dependency injection framework or a service locator, this should be removed.
*/

fun provideDesignerNewsLoginLocalDataSource(context: Context): DesignerNewsLoginLocalDataSource {
fun provideDesignerNewsLoginLocalDataSource(context: Context): LoginLocalDataSource {
val preferences = provideSharedPreferences(
context,
DesignerNewsLoginLocalDataSource.DESIGNER_NEWS_PREF)
return DesignerNewsLoginLocalDataSource(preferences)
LoginLocalDataSource.DESIGNER_NEWS_PREF)
return LoginLocalDataSource(preferences)
}

fun provideDesignerNewsLoginRepository(context: Context): DesignerNewsLoginRepository {
return DesignerNewsLoginRepository.getInstance(
fun provideDesignerNewsLoginRepository(context: Context): LoginRepository {
return LoginRepository.getInstance(
provideDesignerNewsLoginLocalDataSource(context),
provideDesignerNewsLoginRemoteDataSource(context))
}

fun provideDesignerNewsLoginRemoteDataSource(context: Context): DesignerNewsLoginRemoteDataSource {
fun provideDesignerNewsLoginRemoteDataSource(context: Context): LoginRemoteDataSource {
val tokenHolder = provideDesignerNewsAuthTokenLocalDataSource(context)
return DesignerNewsLoginRemoteDataSource(tokenHolder, provideDesignerNewsService(tokenHolder))
return LoginRemoteDataSource(tokenHolder, provideDesignerNewsService(tokenHolder))
}

private fun provideDesignerNewsAuthTokenLocalDataSource(
@@ -23,7 +23,7 @@ import io.plaidapp.core.designernews.data.api.model.User
/**
* Local storage for Designer News login related data, implemented using SharedPreferences
*/
class DesignerNewsLoginLocalDataSource(private val prefs: SharedPreferences) {
class LoginLocalDataSource(private val prefs: SharedPreferences) {

/**
* Instance of the logged in user. If missing, null is returned
@@ -27,7 +27,7 @@ import java.io.IOException
* Remote data source for Designer News login data. Knows which API calls need to be triggered
* for login (auth and /me) and updates the auth token after authorizing.
*/
class DesignerNewsLoginRemoteDataSource(
class LoginRemoteDataSource(
private val tokenLocalDataSource: DesignerNewsAuthTokenLocalDataSource,
val service: DesignerNewsService
) {
@@ -24,9 +24,9 @@ import io.plaidapp.core.designernews.data.api.model.User
* Repository that handles Designer News login data. It knows what data sources need to be
* triggered to login and where to store the data, once the user was logged in.
*/
class DesignerNewsLoginRepository(
private val localDataSource: DesignerNewsLoginLocalDataSource,
private val remoteDataSource: DesignerNewsLoginRemoteDataSource
class LoginRepository(
private val localDataSource: LoginLocalDataSource,
private val remoteDataSource: LoginRemoteDataSource
) {

// local cache of the user object, so we don't retrieve it from the local storage every time
@@ -69,14 +69,14 @@ class DesignerNewsLoginRepository(

companion object {
@Volatile
private var INSTANCE: DesignerNewsLoginRepository? = null
private var INSTANCE: LoginRepository? = null

fun getInstance(
localDataSource: DesignerNewsLoginLocalDataSource,
remoteDataSource: DesignerNewsLoginRemoteDataSource
): DesignerNewsLoginRepository {
localDataSource: LoginLocalDataSource,
remoteDataSource: LoginRemoteDataSource
): LoginRepository {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: DesignerNewsLoginRepository(
INSTANCE ?: LoginRepository(
localDataSource,
remoteDataSource
).also { INSTANCE = it }
@@ -0,0 +1,83 @@
/*
* Copyright 2018 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.plaidapp.core.designernews.login.ui

import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.ViewModel
import io.plaidapp.core.data.CoroutinesContextProvider
import io.plaidapp.core.data.Result
import io.plaidapp.core.designernews.login.data.LoginRepository
import io.plaidapp.core.util.exhaustive
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.launch

/**
* View Model for [DesignerNewsLogin]
* TODO move the rest of the logic from activity here.
* TODO keep this in core for now, to be moved to designernews module
*/
class LoginViewModel(
private val loginRepository: LoginRepository,
private val contextProvider: CoroutinesContextProvider
) : ViewModel() {

private var currentJob: Job? = null

private val _uiState = MutableLiveData<Result<LoginUiModel>>()
val uiState: LiveData<Result<LoginUiModel>>
get() = _uiState

fun login(username: String, password: String) {
// only allow one login at a time
if (currentJob?.isActive == true) {
return
}
currentJob = launchLogin(username, password)
}

private fun launchLogin(username: String, password: String) = launch(contextProvider.io) {
_uiState.postValue(Result.Loading)
val result = loginRepository.login(username, password)

when (result) {
is Result.Success -> {
val user = result.data
val uiModel = LoginUiModel(
user.displayName.toLowerCase(),
user.portraitUrl
)
_uiState.postValue(Result.Success(uiModel))
}
is Result.Error -> _uiState.postValue(result)
is Result.Loading -> {
/* we ignore the loading state */
}
}.exhaustive
}

override fun onCleared() {
super.onCleared()
// when the VM is destroyed, cancel the running job.
currentJob?.cancel()
}
}

/**
* UI model for [DesignerNewsLogin]
*/
data class LoginUiModel(val displayName: String, val portraitUrl: String?)
@@ -75,7 +75,7 @@ object Activities {
* DesignerNewsLogin Activity
*/
object Login : AddressableActivity {
override val className = "$PACKAGE_NAME.ui.designernews.DesignerNewsLogin"
override val className = "$PACKAGE_NAME.designernews.ui.login.DesignerNewsLogin"
}

/**
@@ -14,9 +14,23 @@
* limitations under the License.
*/

package io.plaidapp.ui.designernews.login
package io.plaidapp.core.util

import android.arch.lifecycle.ViewModel
import io.plaidapp.core.designernews.login.data.DesignerNewsLoginRepository

class LoginViewModel(private val loginRepository: DesignerNewsLoginRepository) : ViewModel()
/**
* Helper to force a when statement to assert all options are matched in a when statement.
*
* By default, Kotlin doesn't care if all branches are handled in a when statement. However, if you
* use the when statement as an expression (with a value) it will force all cases to be handled.
*
* This helper is to make a lightweight way to say you meant to match all of them.
*
* Usage:
*
* ```
* when(sealedObject) {
* is OneType -> //
* is AnotherType -> //
* }.exhaustive
*/
val <T> T.exhaustive: T

This comment has been minimized.

Copy link
@rcd27

rcd27 Jul 11, 2019

Damn, this is great! :D

get() = this
@@ -32,17 +32,23 @@ import org.mockito.Mockito
import retrofit2.Response

/**
* Tests for [DesignerNewsLoginRemoteDataSource] using shared preferences from instrumentation
* Tests for [LoginRemoteDataSource] using shared preferences from instrumentation
* context and mocked API service.
*/
class DesignerNewsLoginRemoteDataSourceTest {

private val user = User(id = 3, displayName = "Plaidy Plaidinski", portraitUrl = "www")
private val user = User(
id = 3,
firstName = "Plaidy",
lastName = "Plaidinski",
displayName = "Plaidy Plaidinski",
portraitUrl = "www"
)
private val accessToken = AccessToken("token")

private val service = Mockito.mock(DesignerNewsService::class.java)
private val authTokenDataSource = Mockito.mock(DesignerNewsAuthTokenLocalDataSource::class.java)
private val dataSource = DesignerNewsLoginRemoteDataSource(authTokenDataSource, service)
private val dataSource = LoginRemoteDataSource(authTokenDataSource, service)

@Test
fun logout_clearsToken() {

0 comments on commit a33ba90

Please sign in to comment.