Introduction
, , . , - , . - . , - ... , ?
, . , PullRequest-.
. .
if-else when
Java Android. Kotlin, .
fun getNumberSign(num: Int): String = if (num < 0) {
"negative"
} else if (num > 0) {
"positive"
} else {
"zero"
}
- 7 . :
fun getNumberSign(num: Int): String = when {
num < 0 -> "negative"
num > 0 -> "positive"
else -> "zero"
}
, 5.
if-else
. 2 (Kotlin + Java), . - " …"
:
1.
data class Message(
// ...
val isDelivered: Boolean
)
? ? , Message
? , , ?
2.
data class Message(
// ...
val isDelivered: Boolean,
val isRead: Boolean
)
, ProductOwner . ? - "" . , - . - must have IT . - , , , .
" " , . -> -> -> -> -> -> -> -> . - Boolean ?!!! COVID-19 .
? PO, . , , . , . , ?
3.
4. e.t.c.
data class Message(
// ...
val state: State
) {
enum class State {
SENT,
DELIVERED,
SHOWN_IN_NOTIFICATION,
READ
}
}
. , , … , , , . , ? . , , . , :
data class Message(
// ...
val states: Set<State>
) {
fun hasState(state: State): Boolean = states.contains(state)
}
//
data class Message(
// ...
val states: States
) {
enum class State(internal val flag: Int) {
SENT(1),
DELIVERED(1 shl 1),
READ(1 shl 2),
SHOWN_IN_NOTIFICATION(1 shl 3)
}
data class States internal constructor(internal val flags: Int) {
init {
check(flags and (flags+1)) { "Expected value: flags=2^n-1" }
}
constructor(vararg states: State): this(
states.map(State::flag).reduce { acc, flag -> acc or flag }
)
fun hasState(state: State): Boolean = (flags and state.flag) == state.flag
}
}
: , , . ? ? - - . - . - .
:
data class Message(
//..
val isSent: Boolean,
val isDelivered: Boolean
val isRead: Boolean,
val isShownInNotification: Boolean
)
//...
fun drawStatusIcon(message: Message) {
when {
message.isSent && message.isDelivered && message.isRead && message.isShownInNotification ->
drawNotificationStatusIcon()
message.isSent && message.isDelivered && message.isRead -> drawReadStatusIcon()
message.isSent && message.isDelivered -> drawDeliviredStatusIcon()
else -> drawSentStatus()
}
}
() . .
. , .
data class User(
val username: String?
val hasUsername: Boolean
)
. GUI . , , hasUsername
. , .
// OK
val user1 = User(username = null, hasUsername = false)
// ,
val user2 = User(username = "user", hasUsername = false)
// OK
val user3 = User(username = "user", hasUsername = true)
// , ,
val user4 = User(username = null, hasUsername = true)
// , ,
val user5 = User(username = "", hasUsername = true)
// , ,
val user6 = User(username = " ", hasUsername = true)
. - username
.
data class User(
val username: String?
) {
fun hasUsername(): Boolean = !username.isNullOrBlank()
}
- . , . , . :
data class User(
val username: String?
) {
val hasUsername: Boolean = !username.isNullOrBlank()
val hasUsernameLazy: Boolean by lazy { !username.isNullOrBlank() }
}
.
class UsernameHelper {
private val cache: MutableMap<User, Boolean> = WeakHashMap()
fun hasUsername(user: User): Boolean = cache.getOrPut(user) {
!user.username.isNullOrBlank()
}
}
-
, . , , … . , , , .
3rd party services backend. .
// ...
val result = remoteService.getConfig()
if (result is Result.Success) {
val remoteConfig = result.value.clientConfig?.keys
for (localConfigKey: ConfigKey in configKeyProvider.getConfigKeys()) {
sharedPreferences.edit { putString(localConfigKey.key, remoteConfig[localConfigKey.key]) }
}
}
//...
enum class ConfigKey(val key) {
FACEBOOK("facebook"),
MAPBOX("mapbox"),
THIRD_PARTY("some_service")
}
N , THIRD_PARTY
. , InMemory. 20 . , ?
: InMemory / SharedPreferences / Database / WeakInMemory… . SOLID - , ; open-closed principle , "" .
// ...
val result = remoteService.getConfig()
if (result is Result.Success) {
val remoteConfig = result.value.clientConfig?.keys
for(localConfigKey: ConfigKey in configKeyProvider.getConfigKeys()) {
configurationStorage.put(
configKey = localConfigKey,
keyValue = remoteConfig[localConfigKey.key]
)
}
}
//....
interface ConfigKeyStorage {
fun put(configKey: ConfigKey, keyValue: String?)
fun get(configKey: ConfigKey): String
fun getOrNull(configKey: ConfigKey): String?
}
internal class InMemoryConfigKeyStorage : ConfigKeyStorage {
private val storageMap: MutableMap<ConfigKey, String?> = mutableMapOf()
override fun put(configKey: ConfigKey, keyValue: String?) {
storageMap[configKey] = keyValue
}
override fun get(configKey: ConfigKey): String =
requireNotNull(storageMap[configKey])
override fun getOrNull(configKey: ConfigKey): String? =
storageMap[configKey]
}
, . , . - , . , N , DI/IoC , . , .
. , ( ). , - . . , .
:
, . null
, . , - remote.
interface UsernameRepository {
suspend fun getUsername(): String?
}
class RemoteUsernameRepository(
private val remoteAPI: RemoteAPI
) : UsernameRepository {
override suspend fun getUsername(): String? = try {
remoteAPI.getUsername()
} catch (throwable: Throwable) {
null
}
}
, e String?
String?
. , . getUsername() == null
. , . SuccessState === FailState
.
. , .
, . :
interface UsernameRepository {
suspend fun getUsername(): String?
}
class CommonUsernameRepository(
private val remoteRepository: UsernameRepository,
private val localRepository: UsernameRepository
) : UsernameRepository {
suspend fun getUsername(): String? {
return remoteRepository.getUsername() ?: localRepository.getUsername()
}
}
. 3 . :
,
null
- ? . .
,
null
- ?
,
null
- ?
, . . , ? - . . , , . - . , .
- .
enum
/sealed classes
/interfaces
/abstract classes
. . - enum
/sealed classes
. - interface
/abstract classes
.
sealed class UsernameState {
data class Success(val username: CharSequence?) : UsernameState()
object Failed : UsernameState()
}
When
- , .
enum class NavigationFlow {
PIN_CODE,
MAIN_SCREEN,
ONBOARDING,
CHOOSE_LANGUAGE
}
fun detectNavigationFlow(): NavigationFlow {
return when {
authRepo.isAuthorized() -> NavigationFlow.PIN_CODE
languageRepo.defaultLanguage != null -> NavigationFlow.CHOOSE_LANGUAGE
onboardingStorage.isCompleted() -> NavigationFlow.MAIN_SCREEN
else -> NavigationFlow.ONBOARDING
}
}
. . detectNavigationFlow
. : , … , , .
enum class NavigationFlow {
PIN_CODE,
MAIN_SCREEN,
ONBOARDING,
CHOOSE_LANGUAGE
}
//
sealed class State {
data class Found(val flow: NavigationFlow) : State()
object NotFound : State()
}
interface NavigationFlowProvider {
// null NavigationFlow
fun getNavigation(): NavigationFlow
}
//
interface NavigationFlowResolver {
fun resolveNavigation(): State
}
internal class SplashScreenNavigationFlowProvider(
// Sequence - .
// .
private val resolvers: Sequence<NavigationFlowResolver>
) : NavigationFlowProvider {
override fun getNavigation(): NavigationFlow = resolvers
.map(NavigationFlowResolver::resolveNavigation)
.filterIsInstance<State.Found>()
.firstOrNull()?.flow
// -
?: NavigationFlow.MAIN_SCREEN
}
N- when
ChainOfResponsibililty. : . :
SOLID
,
- . . . , . . DIP , .
BaseActivity. , , . . , , . , - N , … , . - , . BaseActivity
- … > 1000 , . SOLID .
, , 2. ?
, Android SDK 14 . Application.ActivityLifecycleCallbacks
, Activity
. .
class App : Application(), KoinComponent {
override fun onCreate() {
super.onCreate()
// ...
registerActivityLifecycleCallbacks(SetupKoinFragmentFactoryCallbacks())
}
// Koin FragmentFactory Koin
private class SetupKoinFragmentFactoryCallbacks : EmptyActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (activity is FragmentActivity) {
activity.setupKoinFragmentFactory()
}
}
}
}
, . :
abstract class BaseActivity(@LayoutRes contentLayoutId: Int = 0) : AppCompatActivity(contentLayoutId) {
// attachBaseContext protected
override fun attachBaseContext(newBase: Context) {
// extension
super.attachBaseContext(newBase.applySelectedAppLanguage())
}
}
BaseFragment. . FragmentManager, registerFragmentLifecycleCallbacks - . FragmentLifecycleCallbacks
- Activty
. Koin - .
. DIP - Dagger, Koin, .. . ? - ? 5 2018 , FragmentFactory .
BaseApplication. . Flavors
BuildType
Application
. , Application
, , 3rd party . , .
interface Bootstrapper {
// KoinComponent - entry point DIP
fun init(component: KoinComponent)
}
interface BootstrapperProvider {
fun provide(): Set<Bootstrapper>
}
class BootstrapperLauncher(val provider: BootstrapperProvider) {
fun launch(component: KoinComponent) {
provider.provide().onEach { it.init(component) }
}
}
class App : Application() {
override fun onCreate() {
super.onCreate()
// Koin
this.get<BootstrapperLauncher>().launch(component = this)
}
}
- . , . . (), , . , . , - , ?
, .
interface Validator {
fun validate(contact: CharSequence): ValidationResult
}
sealed class ValidationResult {
object Valid : ValidationResult()
data class Invalid(@StringRes val errorRes: Int) : ValidationResult()
}
class PhoneNumberValidator : Validator {
override fun validate(contact: CharSequence): ValidationResult =
if (REGEX.matches(contact)) ValidationResult.Valid
else ValidationResult.Invalid(R.string.error)
companion object {
private val REGEX = "[0-9]{16}".toRegex()
}
}
, ? , . . , "" , .
class PhoneNumberValidator : Validator {
override fun validate(contact: CharSequence): ValidationResult =
if (REGEX.matches(contact)) ValidationResult.Valid
else ValidationResult.Invalid(R.string.error)
companion object {
private val REGEX = "+[0-9]{16}".toRegex()
}
}
, , . , . , . .
, :
, , MSISDN .
interface Validator {
fun validate(contact: CharSequence): ValidationResult
}
sealed class ValidationResult {
object Valid : ValidationResult()
data class Invalid(@StringRes val errorRes: Int) : ValidationResult()
}
internal class MSISDNNumberValidator : Validator {
//...
}
internal class E164NumberValidator : Validator {
//...
}
, , . :
interface ValidatorFactory {
fun create(type: ValidatorType): Validator?
interface ValidatorType
companion object {
fun create() : ValidatorFactory {
return DefaultValidatorFactory()
}
}
}
object MSISDN : ValidatorFactory.ValidatorType
object E164 : ValidatorFactory.ValidatorType
private class DefaultValidatorFactory : ValidatorFactory {
override fun create(type: ValidatorFactory.ValidatorType): Validator? = when(type) {
is MSISDN -> MSISDNValidator()
is E164 -> E164Validator()
else -> null
}
}
, . , . - ValidatorFactory
DefaultValidatorFactory
. .
, , SOLID. , . . ? . . . - . , - . , . 2-3 . enterprise. - . .