[Dicas do Kotlin] Cenários de uso comissionados e dicas de desenvolvimento

Este artigo classifica o que eu acho que são dicas de desenvolvimento encomendadas pelo Kotlin, que não são necessariamente as melhores práticas e são apenas para referência de aprendizado.

Geralmente, há duas maneiras de usar delegados Kotlin:

  • Delegação de classe, a função da classe é delegada a outras classes.
  • Delegação de atributo, confiando um domínio a uma classe para encapsular.

Desta vez, darei alguns pequenos exemplos para ajudar a organizar melhor a arquitetura e o desenvolvimento.

delegação de classe

Delegação de classe em Kotlin refere-se a delegar a funcionalidade de uma classe para outras classes.

class ApiDelegate(impl: ApiImpl) : Api by impl

interface Api {
    
    
    fun doSomething()
}

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

A classe delegada pode substituir completamente o objeto delegado e todos os métodos do objeto delegado serão sobrecarregados pela classe delegada. A classe delegada pode controlar o processo de inicialização do objeto delegado por meio de seu próprio construtor. Esse mecanismo pode byser implementado usando palavras-chave. Ao usá-lo pode ser usado das seguintes maneiras:

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

Qual é a utilidade deste método?

Quando vi essas linhas de código, a pergunta me veio à mente: para que serve esse tipo de delegação? Com esta questão em mente, vamos olhar para baixo.

Estenda a funcionalidade da classe

herdar

Em um cenário semelhante à API, geralmente projetamos uma interface e escrevemos várias classes de implementação, que são diferentes lógicas de implementação ou classes de teste.

Quando o usamos, geralmente usamos apenas a interface e não nos importamos e não devemos nos preocupar com os detalhes de implementação da interface. Isso aumentará muito a dissociação , testabilidade e manutenção do código .

Ao projetar uma classe, se ela não for projetada para ser herdada, crie a classe para proibir a herança . Ou seja, em Java final class, as classes em Kotlin não são herdáveis ​​por padrão. Quando uma classe é herdada abertamente, ela traz grandes riscos, por exemplo:

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 // 不报错
}

Neste código, declaro o uso Datana classe para restringir seu acesso. Quando eu herdo a classe e a exponho, o exterior pode obter os dados internos. Isso não ajuda a proteger a segurança dos dados da classe. Embora essa regravação não afete a compilação, ela pode violar o princípio da menor visibilidade. Portanto, as classes precisam ser consideradas tanto quanto possível ao projetar classes .valueprotectedDatavaluefinal

composição em vez de herança

Quando precisamos estender ou modificar uma classe final mantendo sua API herdada, podemos usar o método de combinação para alcançá-lo:

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
    }
}

Essa nova classe de implementação retém parte ApiImplda lógica e implementa a lógica da nova interface. Ou seja, parte da lógica é delegada para apiImplessa instância, que é um pouco como o modo proxy.Na verdade, a delegação também pode ser usada como modo proxy, e é totalmente automática , que é o exemplo do início. No Kotlin, você pode usar bypalavras-chave para simplificar essa lógica.

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

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

A implementação do compilador gerará um código semelhante ao anterior, confiará a lógica na interface à classe de implementação e, se a interface for reescrita, ela será calculada de acordo com a nova lógica reescrita.

Isso é um pouco como herança, mas é realizado por combinação. Na verdade, não é uma ApiImplclasse herdada. Não pode ser usada ApiImplcomo uma classe. Pode evitar ApiImplprojetar uma classe como uma classe herdável. Ao mesmo tempo, novas classes pode adicionar novas funções ou herdar outras classes .

Injeção de dependência

O código acima usa o método hardcode para delegar. Caso não seja necessário no desenvolvimento real, é recomendável extrair a classe de implementação do delegado no construtor, conforme mostrado abaixo:

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

As classes criadas dessa maneira são melhores em termos de capacidade de manutenção e testabilidade . No NewApiImplprocesso de desenvolvimento, não há necessidade de se preocupar e não se preocupar com os detalhes de implementação da API.

Quando preciso testar essa classe com a API de teste no teste de unidade, posso escrever o seguinte código:

class TestApiImpl: Api {
    
     ... }

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

dicas de delegação de classe

Além do uso básico acima para reduzir o código clichê, no desenvolvimento real, ele também pode ajudar as funções de inicialização a reduzir uma grande quantidade de código lógico.

Por exemplo, se eu precisar que uma fila tenha anti-vibração por um determinado período de tempo, faça certa lógica antes e depois do anti-vibração.

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

channel.tryEmit(1)

Quando essa lógica precisa ser reutilizada em vários locais, é muito desconfortável escrever uma sequência tão grande de códigos.Você pode usar a delegação de classe para extrair toda a lógica padronizada para uma 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)
    }
}

Ao mesmo tempo, você pode usar funções de extensão para reduzir a entrada de parâmetros de escopo de corrotina:

@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)

Basta declará-lo diretamente ao usá-lo:

@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)
    }

}

Deve-se observar que embora esse método de uso seja conveniente, ele possui uma desvantagem, ou seja, perde o encapsulamento da lógica. Por exemplo, o gerado acima é uma instância real do Canal. Se o desenvolvedor o usar incorretamente e usá-lo como um Canal normal, pode causar alguns problemas estranhos.

delegação de propriedade

Existem mais informações sobre o uso de delegação de atributos e mais informações podem ser consultadas. Recomenda-se verificar a documentação oficial . Vou apresentar brevemente aqui. Através de bypalavras-chave, um atributo pode ser delegado para outra classe, e as funções de valor recuperação e atribuição podem ser delegadas a esta classe, classe getValuee setValuefunção.

Por exemplo, no desenvolvimento do Compose, geralmente escrevemos o seguinte código:

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

setValueA busca e a atribuição de valor aqui não obtêm diretamente o State, mas chamam as funções e de extensão no State getValuepara obter o valor Stateno state valuee copiá-lo para Stateo state 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

A interface oficial dessas duas funções é fornecida, ReadOnlyPropertyapenas legível, ReadWritePropertygravável e legível :

// 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)
}

setValueQuando funções e funções são implementadas getValue, a palavra-chave delegate pode ser usada by.

Dicas de delegação de propriedade

delegação de estado da IU

No desenvolvimento do Compose, geralmente usamos a arquitetura MVI intencionalmente ou não. Na camada ViewModel, alguns estados da interface do usuário são mantidos para a camada de exibição monitorar e os estados da interface do usuário contêm muitos estados e lógica. Por exemplo, este exemplo é brevemente apresentado em meu artigo anterior:

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

Ao manter o estado no ViewModel, se o estado for relativamente simples, podemos usar Compose State, e para estado e lógica mais complexos, geralmente usamos Flowou StateFlow, por exemplo, preciso monitorar as alterações de dados do banco de dados ou da extremidade remota, o camada subjacente é exposta Flow, podemos usar mapou combinefazer a transição para o estado da interface do usuário neste ponto.

Em suma, precisamos apenas do estado da interface do usuário e não me importo com outras lógicas relacionadas, caso contrário, a camada ficará ViewModelmuito inchada. do seguinte modo:

class NoteViewModel : ViewModel() {
    
    

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

}

Portanto, podemos agrupar essa parte da lógica e implementar a interface delegada:

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

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

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

}

Não se preocupe com outra lógica ao usá-lo:

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

Geralmente, a lógica como confluência e conversão é inevitável ao usar estados Flow, mas usar delegação é extremamente conveniente.No exemplo acima, um estado Scafold requer dois estados de interface do usuário, estado de conteúdo e estado da barra inferior. A lógica dessas duas classes pode ser delegada à classe correspondente para implementação.

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
    }
}

A instância pode ser obtida através do parâmetro thisRef ViewModel. Neste momento é muito conveniente obter viewModelScopeo escopo da co-rotina, em alguns casos também pode reduzir o número de parâmetros passados ​​e aumentar a simplicidade do código.

Outra coisa a se notar é que as propriedades geradas por meio da delegação não podem obter nenhuma lógica ou detalhes da classe delegada, mesmo que você exponha acidentalmente qual propriedade ou API (embora não seja recomendado fazer isso), não importa.

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
}

Se quiser, você pode usá-lo com o Dagger Hilt para evitar que as dependências injetadas sejam expostas.

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
    }

}

Resumir

A delegação Kotlin é um recurso avançado que reduz o código clichê, mas é muito flexível no uso real. Além do uso oficial lazye ObservablePropertymais comum, pode ser usado em conjunto com ViewBinding, etc. Este artigo também apresenta dois cenários de desenvolvimento reais para todos se referem. Acredito que depois de ler este artigo, você terá uma compreensão mais ampla sobre o uso da atribuição Kotlin e não ficará na fase em que já ouviu falar, mas não pode usar.

afinal

Se você deseja se tornar um arquiteto ou deseja ultrapassar a faixa salarial de 20 a 30 mil, não se limite a codificação e negócios, mas deve ser capaz de selecionar modelos, expandir e melhorar o pensamento de programação. Além disso, um bom plano de carreira também é muito importante, e o hábito de aprender é muito importante, mas o mais importante é saber perseverar.Qualquer plano que não pode ser implementado de forma consistente é conversa fiada.

Se você não tem direção, gostaria de compartilhar com você um conjunto de "Notas avançadas sobre os oito principais módulos do Android", escrito pelo arquiteto sênior de Ali, para ajudá-lo a organizar sistematicamente o conhecimento confuso, disperso e fragmentado, de modo que você possa dominar de forma sistemática e eficiente os vários pontos de conhecimento do desenvolvimento do Android.
img
Em comparação com o conteúdo fragmentado que costumamos ler, os pontos de conhecimento desta nota são mais sistemáticos, mais fáceis de entender e lembrar e são organizados estritamente de acordo com o sistema de conhecimento.

Bem-vindo a todos para apoiar com um clique e três links. Se precisar das informações no artigo, você pode digitalizar diretamente o cartão WeChat de certificação oficial da CSDN no final do artigo para obtê-lo gratuitamente↓↓↓

PS: Existe também um robô ChatGPT no grupo, que pode responder ao seu trabalho ou questões técnicas

Acho que você gosta

Origin blog.csdn.net/datian1234/article/details/131404357
Recomendado
Clasificación