[Astuces Kotlin] Scénarios d'utilisation commandés et conseils de développement

Cet article trie ce que je pense être des conseils de développement commandés par Kotlin, qui ne sont pas nécessairement les meilleures pratiques et ne sont que des références d'apprentissage.

Il existe généralement deux façons d'utiliser les délégués Kotlin :

  • Délégation de classe, la fonction de la classe est déléguée à d'autres classes.
  • Délégation d'attribut, confiant un domaine à une classe à encapsuler.

Cette fois, je vais donner quelques petits exemples pour aider à mieux organiser l'architecture et le développement.

délégation de classe

La délégation de classe dans Kotlin fait référence à la délégation de la fonctionnalité d'une classe à d'autres classes.

class ApiDelegate(impl: ApiImpl) : Api by impl

interface Api {
    
    
    fun doSomething()
}

class ApiLocalImpl : Api {
    
    
    override fun doSomething() = TODO("Not yet implemented")
}

La classe déléguée peut complètement remplacer l'objet délégué et toutes les méthodes de l'objet délégué seront surchargées par la classe déléguée. La classe déléguée peut contrôler le processus d'initialisation de l'objet délégué via son propre constructeur. Ce mécanisme peut byêtre implémenté en utilisant des mots clés. Lors de son utilisation, il peut être utilisé des manières suivantes:

val apiImpl = ApiImpl()
val apiDelegate = ApiDelegate(apiImpl)
apiDelegate.doSomething() // equals to apiImpl do something.

A quoi sert cette méthode ?

Quand j'ai vu ces lignes de code, la question m'est venue à l'esprit, à quoi sert ce genre de délégation ? Avec cette question à l'esprit, regardons vers le bas.

Étendre les fonctionnalités de la classe

hériter

Dans un scénario de type API, nous concevons généralement une interface et écrivons plusieurs classes d'implémentation, qui sont soit une logique d'implémentation différente, soit des classes de test.

Lorsque nous l'utilisons, nous n'utilisons généralement que l'interface, et nous ne nous soucions pas et ne devrions pas nous soucier des détails d'implémentation de l'interface. Cela améliorera considérablement le découplage , la testabilité et la maintenabilité du code .

Lors de la conception d'une classe, si elle n'est pas conçue pour être héritée, concevez la classe pour interdire l'héritage . Autrement dit, en Java final class, les classes de Kotlin ne peuvent pas être héritées par défaut. Lorsqu'une classe est ouvertement héritée, cela comporte de grands risques. Par exemple :

open class Data {
    
    
    protected open val value: Int = 0
}

class OwData : Data() {
    
    
    public override val value: Int = 1
}

fun main() {
    
    
    val data = Data()
    data.value   // 报错
    val owData = OwData()
    owData.value // 不报错
}

Dans ce code, je déclare l'utilisation Datadans la classe pour restreindre son accès. Lorsque j'hérite de la classe et que je l'expose, l'extérieur peut obtenir les données à l'intérieur. Ceci n'est pas propice à la protection de la sécurité des données de la classe, bien que cette réécriture n'affecte pas la compilation, elle peut violer le principe de moindre visibilité. Par conséquent, les classes doivent être prises en compte autant que possible lors de la conception des classes .valueprotectedDatavaluefinal

composition au lieu d'héritage

Lorsque nous devons étendre ou modifier une classe finale tout en conservant son API héritée, nous pouvons utiliser la méthode de combinaison pour y parvenir :

interface Api {
    
    
    fun doSomething()
    fun doSomething2()
}

interface Api2 {
    
    
    fun newApiLogic()
}

class NewApiImpl : Api, Api2 {
    
    
    private val apiImpl = ApiImpl()

    // 保留原有逻辑
    override fun doSomething() = apiImpl.doSomething()

    override fun doSomething2() {
    
    
        // 修改接口
    }

    // 扩展新功能
    override fun newApiLogic() {
    
    
        // new logic
    }
}

Cette nouvelle classe d'implémentation conserve une partie ApiImplde la logique et implémente la logique de la nouvelle interface. En d'autres termes, une partie de la logique est déléguée à apiImplcette instance, ce qui est un peu comme le mode proxy.En fait, la délégation peut également être utilisée comme mode proxy, et c'est entièrement automatique , ce qui est l'exemple du début. Dans Kotlin, vous pouvez utiliser bydes mots clés pour simplifier cette logique.

class NewApiImpl : Api by ApiImpl(), Api2 {
    
    
    
    // 省略重写doSomething逻辑

    override fun doSomething2() {
    
     /* change logic */ }
    override fun newApiLogic() {
    
     /* new logic */ }
}

L'implémentation du compilateur générera un code similaire au précédent, confiera la logique dans l'interface à la classe d'implémentation, et si l'interface est réécrite, elle sera calculée selon la logique nouvellement réécrite.

C'est un peu comme l'héritage, mais c'est réalisé par combinaison. En fait, ce n'est pas une ApiImplclasse héritée. Elle ne peut pas être utilisée ApiImplcomme une classe. Cela peut éviter ApiImplde concevoir une classe comme une classe héritable. En même temps, de nouvelles classes peut ajouter de nouvelles fonctions ou hériter d'autres classes .

injection de dépendance

Le code ci-dessus utilise la méthode de code dur pour déléguer. Si cela n'est pas nécessaire dans le développement réel, il est recommandé d'extraire la classe d'implémentation déléguée dans le constructeur, comme indiqué ci-dessous :

class NewApiImpl(api: Api) : Api by api, APi2 {
    
    
    ....
}

Les classes créées de cette manière sont meilleures en termes de maintenabilité et de testabilité . Dans NewApiImplle processus de développement, il n'est pas nécessaire de se soucier et ne peut pas se soucier des détails de mise en œuvre de l'API.

Lorsque j'ai besoin de tester cette classe avec l'API de test dans le test unitaire, je peux écrire le code suivant :

class TestApiImpl: Api {
    
     ... }

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    
    
    @Test
    fun testFeature() {
    
    
        val testApiImpl = TestApiImpl()
        val newApi: Api = NewApiImpl(testApiImpl)
        // testLogic
    }
}

Conseils de délégation de classe

En plus de l'utilisation de base ci-dessus pour réduire le code passe-partout, dans le développement réel, il peut également aider les fonctions d'initialisation à réduire une grande quantité de code logique.

Par exemple, si j'ai besoin d'une file d'attente pour avoir un anti-shake pendant une certaine période de temps, faites une certaine logique avant et après l'anti-shake.

val channel = Channel<Int>(0, BufferOverflow.DROP_OLDEST).apply {
    
    
    consumeAsFlow()
        .onEach {
    
    
            // pre logic
        }.debounce(500)
        .onEach {
    
    
            // after logic
        }.launchIn(coroutineScope)
}

channel.tryEmit(1)

Lorsque cette logique doit être réutilisée à plusieurs endroits, il est très inconfortable d'écrire une si grande chaîne de codes.Vous pouvez utiliser la délégation de classe pour extraire toute la logique passe-partout dans une classe.

@OptIn(FlowPreview::class)
class DebounceActionChannel<T>(
    coroutineScope: CoroutineScope,
    debounceTimeMillis: Long = 500L,
    preEach: (suspend (T) -> Unit)? = null,
    action: suspend (T) -> Unit,
) : Channel<T> by Channel(
    capacity = 0,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
) {
    
    
    init {
    
    
        consumeAsFlow().run {
    
    
            if (preEach != null) onEach(preEach) else this
        }.debounce(debounceTimeMillis)
            .onEach(action)
            .launchIn(coroutineScope)
    }
}

Dans le même temps, vous pouvez utiliser des fonctions d'extension pour réduire l'entrée des paramètres de portée de la coroutine :

@Suppress("FunctionName")
fun <T> ViewModel.DebounceActionChannel(
    debounceTimeMillis: Long = 500L,
    preEach: (suspend (T) -> Unit)? = null,
    action: suspend (T) -> Unit
): Channel<T> = DebounceActionChannel(viewModelScope, debounceTimeMillis, preEach, action)

@Suppress("FunctionName")
fun <T> LifecycleOwner.DebounceActionChannel(
    debounceTimeMillis: Long = 500L,
    preEach: (suspend (T) -> Unit)? = null,
    action: suspend (T) -> Unit
): Channel<T> = DebounceActionChannel(lifecycleScope, debounceTimeMillis, preEach, action)

Il suffit de le déclarer directement lors de son utilisation :

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val homePageDataHolder: HomePageDataHolder
) : ViewModel() {
    
    

    val dataStoreHomePage get() = homePageDataHolder.homePage.value.takeIf {
    
     it != -1 } ?: 0

    private val updateHomePageChannel = DebounceActionChannel(
        debounceTimeMillis = 1000L,
        action = homePageDataHolder::setHomePage
    )

    fun updateHomePage(homePage: Int) {
    
    
        updateHomePageChannel.trySend(homePage)
    }

}

Il convient de noter que bien que cette méthode d'utilisation soit pratique, elle présente un inconvénient, c'est-à-dire qu'elle perd l'encapsulation de la logique. Par exemple, le produit généré ci-dessus est une véritable instance de canal. Si le développeur l'utilise de manière incorrecte et l'utilise comme un canal normal, cela peut entraîner des problèmes étranges.

délégation de propriété

Il y a plus d'informations sur l'utilisation de la délégation d'attribut et plus d'informations peuvent être consultées. Il est recommandé de consulter la documentation officielle . Je vais brièvement la présenter ici. Grâce à bydes mots-clés, un attribut peut être délégué à une autre classe, et les fonctions de valeur la récupération et l'affectation peuvent être déléguées à cette classe getValueet setValuefonction.

Par exemple, dans le développement Compose, nous écrivons souvent le code suivant :

var isExpended by remember {
    
     mutableStateOf(false) }
Button(
    onClick = {
    
    
        isExpended = !isExpended
    }
) {
    
     
    if (isExpended) {
    
    
    }
}

setValueLa récupération et l'affectation de valeur ici n'obtiennent pas directement l'état, mais appellent les fonctions d'extension et dans l'état getValuepour obtenir la valeur Statedans l'état valueet la copier dans Statel'état value.

// SnapshotState.kt

inline operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
    
    
    this.value = value
}

inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value

L'interface officielle de ces deux fonctions est fournie, ReadOnlyPropertyuniquement lisible, ReadWritePropertyinscriptible et lisible :

// Interfaces.kt

public fun interface ReadOnlyProperty<in T, out V> {
    
    
    public operator fun getValue(thisRef: T, property: KProperty<*>): V
}

public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
    
    
    public override operator fun getValue(thisRef: T, property: KProperty<*>): V
    public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}

setValueLorsque les fonctions et les fonctions sont implémentées getValue, le mot - clé délégué peut être utilisé by.

Conseils sur la délégation de propriété

Délégation d'état de l'interface utilisateur

Dans le développement Compose, nous utilisons souvent l'architecture MVI intentionnellement ou non. Dans la couche ViewModel, certains états de l'interface utilisateur sont conservés pour être surveillés par la couche View, et les états de l'interface utilisateur contiennent de nombreux états et logiques. Par exemple, cet exemple est brièvement présenté dans mon article précédent :

data class NoteScaffoldState(
    val contentState: NoteContentState = NoteContentState(),
    val bottomBarState: NoteBottomBarState = NoteBottomBarState()
)

Lors du maintien de l'état dans ViewModel, si l'état est relativement simple, nous pouvons utiliser Compose State, et pour un état et une logique plus complexes, nous utilisons généralement Flowor StateFlow, par exemple, j'ai besoin de surveiller les modifications de données de la base de données ou de l'extrémité distante, le la couche sous-jacente est exposée Flow, nous pouvons utiliser mapou combinepasser à l'état de l'interface utilisateur à ce stade.

Dans l'ensemble, nous n'avons besoin que de l'état de l'interface utilisateur, et je me fiche des autres logiques connexes, sinon la couche sera ViewModeltrès gonflée. comme suit:

class NoteViewModel : ViewModel() {
    
    

    val uiState: StateFlow<NoteScreenState> // 只需要这个状态,不关心逻辑

}

Par conséquent, nous pouvons envelopper cette partie de la logique et implémenter l'interface délégué :

class NoteRouteStateFlowDelegate(
    // 一堆参数
) : ReadOnlyProperty<ViewModel, StateFlow<NoteScreenState>> {
    
    

    // 一堆逻辑
    
    private val noteScreenState: StateFlow<NoteScreenState>

    override fun getValue(thisRef: ViewModel, property: KProperty<*>): StateFlow<NoteScreenState> {
    
    
        return noteScreenState
    }

}

Ne vous souciez pas d'une autre logique lorsque vous l'utilisez :

val uiState: StateFlow<NoteScreenState> by NoteRouteStateFlowDelegate(...)

Généralement, une logique telle que la confluence et la conversion est inévitable lors de l'utilisation des états Flow, mais l'utilisation de la délégation est extrêmement pratique. Dans l'exemple ci-dessus, un état Scafold nécessite deux états d'interface utilisateur, l'état du contenu et l'état de la barre inférieure. La logique de ces deux classes peut être déléguée à la classe correspondante pour implémentation.

class NoteRouteStateFlowDelegate(...) : ReadOnlyProperty<ViewModel, StateFlow<NoteScreenState>> {
    
    

    private val noteBottomBarState: StateFlow<NoteBottomBarState>
        by NoteBottomStateFlowDelegate(...)
    private val noteContentState: StateFlow<NoteContentState?>
            by NoteContentStateFlowDelegate(...)
            
    private val noteScreenState: StateFlow<NoteScreenState> = combine(
        noteContentState.filterNotNull(), noteBottomBarState
    ) {
    
     noteContentState, noteBottomBarState ->
        NoteScreenState.State(
            NoteScaffoldState(
                contentState = noteContentState,
                bottomBarState = noteBottomBarState
            )
        )
    }.stateIn(...)

    override fun getValue(thisRef: ViewModel, property: KProperty<*>): StateFlow<NoteScreenState> {
    
    
        return noteScreenState
    }
}

// 其他委托类
class NoteContentStateFlowDelegate(...)
    : ReadOnlyProperty<Any?, StateFlow<NoteContentState?>> {
    
    

    private val noteContentStateFlow: StateFlow<NoteContentState?>
    
    override fun getValue(
        thisRef: Any?,
        property: KProperty<*>
    ): StateFlow<NoteContentState?> {
    
    
        return noteContentStateFlow
    }
}

L'instance peut être obtenue via le paramètre thisRef ViewModel. A ce stade, il est très pratique d'obtenir viewModelScopela portée de la coroutine. Dans certains cas, cela peut également réduire le nombre de paramètres passés et augmenter la simplicité du code.

Une autre chose à noter est que les propriétés générées par délégation ne peuvent obtenir aucune logique ou détails de la classe déléguée, même si vous exposez accidentellement quelle propriété ou API (bien qu'il ne soit pas recommandé de le faire), cela n'a pas d'importance.

class DataDelegate : ReadOnlyProperty<Any?, String> {
    
    

    val file: File = File("") // 不小心暴露了个属性

    fun api() {
    
    
        /* 不小心暴露了个API */
    }

    private val value: String = ""

    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
    
    
        return value
    }

}

fun main() {
    
    
    val data: String by DataDelegate()
    // 此处无法获取到不小心暴露出来的 file 和 api
}

Si vous le souhaitez, vous pouvez l'utiliser avec Dagger Hilt pour éviter que les dépendances injectées ne soient exposées.

class DataDelegate @Inject constructor() : ReadOnlyProperty<Any?, String> {
    
    

    @Inject
    lateinit var someUseCase: UseCase

    private val value: String = ""

    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
    
    
        return value
    }

}

Résumer

La délégation Kotlin est une fonctionnalité avancée qui réduit le code passe-partout, mais elle est très flexible dans l'utilisation réelle. En plus de l'utilisation officielle lazyet ObservablePropertyplus courante, elle peut être utilisée conjointement avec ViewBinding, etc. Cet article présente également deux scénarios de développement réels pour tout le monde se réfère. Je crois qu'après avoir lu cet article, vous aurez une compréhension plus large de l'utilisation de la confiance Kotlin, et vous ne resterez pas au stade où vous l'avez entendu mais ne pouvez pas l'utiliser.

enfin

Si vous souhaitez devenir architecte ou franchir la fourchette des salaires de 20 à 30 000, ne vous limitez pas au codage et aux affaires, mais vous devez être en mesure de sélectionner des modèles, d'élargir et d'améliorer la réflexion sur la programmation. De plus, un bon plan de carrière est également très important, et l'habitude d'apprendre est très importante, mais le plus important est de pouvoir persévérer.Tout plan qui ne peut pas être mis en œuvre de manière cohérente est un vain mot.

Si vous n'avez pas de direction, je voudrais ici partager avec vous un ensemble de "Notes avancées sur les huit modules majeurs d'Android" rédigées par l'architecte principal d'Ali, pour vous aider à organiser systématiquement les connaissances désordonnées, dispersées et fragmentées, afin que vous puissiez maîtriser systématiquement et efficacement les différents points de connaissance du développement Android.
image
Par rapport au contenu fragmenté que nous lisons habituellement, les points de connaissance de cette note sont plus systématiques, plus faciles à comprendre et à mémoriser, et sont organisés strictement selon le système de connaissances.

Bienvenue à tous au support en un clic et trois liens. Si vous avez besoin des informations contenues dans l'article, vous pouvez scanner directement la carte WeChat de certification officielle CSDN à la fin de l'article pour l'obtenir gratuitement↓↓↓

PS : Il y a aussi un robot ChatGPT dans le groupe, qui peut répondre à vos questions professionnelles ou techniques

Je suppose que tu aimes

Origine blog.csdn.net/datian1234/article/details/131404357
conseillé
Classement