译livedata-flow在mvvm中的最佳实践

eclipse_xu eclipse_xu     2023-01-14     775

关键词:

点击上方蓝字关注我,知识会给你力量

最近在Medium上看到了Flow开发者写的几篇文章,觉得很不错,推荐给大家。

1

原文链接:https://proandroiddev.com/using-livedata-flow-in-mvvm-part-i-a98fe06077a0

最近,我一直在寻找MVVM架构中Kotlin Flow的最佳实践。在我回答了这个关于LiveData和Flow的问题后,我决定写这篇文章。在这篇文章中,我将解释如何在MVVM模式中使用Flow与LiveData。然后我们将看到如何通过使用Flow来改变应用程序的主题。

sample地址:https://github.com/fgiris/LiveDataWithFlowSample

什么是Flow?

Flow是coroutines库中的一个反应式流,能够从一个Suspend函数中返回多个值。

尽管Flow的用法似乎与LiveData非常相似,但它有更多的优势,比如:

  • 本身是异步的,具有结构化的并发性

  • 用map、filter等操作符简单地转换数据

  • 易于测试

如何在MVVM中使用Flow

如果你的应用程序有MVVM架构,你通常有一个数据层(数据库、数据源等)、ViewModel和View(Fragment或Activity)。你可能会使用LiveData在这些层之间进行数据传输和转换。但LiveData的主要目的是什么?它是为了进行数据转换而设计的吗?

LiveData从来没有被设计成一个完全成熟的反应式流构建器

——Jose Alcérreca在2019年Android Dev峰会上说

由于LiveData是一个具有生命周期意识的组件,因此最好在View和ViewModel层中使用它。但数据层呢?我认为在数据库层使用LiveData的最大问题是所有的数据转换都将在主线程上完成,除非你启动一个coroutine并在里面进行工作。这就是为什么你可能更喜欢在数据层中使用Suspend函数。

假设你想从网络上获取天气预报数据。那么在你的数据库中使用Suspend函数就会类似于下面的情况。

class WeatherForecastRepository @Inject constructor() 
    suspend fun fetchWeatherForecast(): Result<Int> 
        // Since you can only return one value from suspend function
        // you have to set data loading before calling fetchWeatherForecast

        // Fake api call
        delay(1000)

        // Return fake success data 
        return Result.Success((0..20).random())
    

你可以在ViewModel中用viewModelScope调用这个函数。

class WeatherForecastOneShotViewModel @Inject constructor(
    val weatherForecastRepository: WeatherForecastRepository
) : ViewModel() 

    private var _weatherForecast = MutableLiveData<Result<Int>>()
    val weatherForecast: LiveData<Result<Int>>
        get() = _weatherForecast

    fun fetchWeatherForecast() 
        // Set value as loading
        _weatherForecast.value = Result.Loading

        viewModelScope.launch 
            // Fetch and update weather forecast LiveData
            _weatherForecast.value = weatherForecastRepository.fetchWeatherForecast()
        
    

这种方法对于每次被调用时都会运行的单次请求来说效果不错。但是在获取数据流的时候呢?

这里就是Flow发挥作用的地方。如果你想从你的服务器上获取实时更新,你可以用Flow来做,而不用担心资源的泄露,因为结构化的并发性迫使你这样做。

让我们转换我们的数据库,使其返回Flow。

class WeatherForecastRepository @Inject constructor() 

    /**
     * This methods is used to make one shot request to get
     * fake weather forecast data
     */
    fun fetchWeatherForecast() = flow 
        emit(Result.Loading)
        // Fake api call
        delay(1000)
        // Send a random fake weather forecast data
        emit(Result.Success((0..20).random()))
    

    /**
     * This method is used to get data stream of fake weather
     * forecast data in real time
     */
    fun fetchWeatherForecastRealTime() = flow 
        emit(Result.Loading)
        // Fake data stream
        while (true) 
            delay(1000)
            // Send a random fake weather forecast data
            emit(Result.Success((0..20).random()))
        
    

现在,我们能够从一个Suspend函数中返回多个值。你可以使用asLiveData扩展函数在ViewModel中把Flow转换为LiveData。

class WeatherForecastOneShotViewModel @Inject constructor(
    weatherForecastRepository: WeatherForecastRepository
) : ViewModel() 

    private val _weatherForecast = weatherForecastRepository
        .fetchWeatherForecast()
        .asLiveData(viewModelScope.coroutineContext) // Use viewModel scope for auto cancellation

    val weatherForecast: LiveData<Result<Int>>
        get() = _weatherForecast

这看起来和使用LiveData差不多,因为没有数据转换。让我们看看从数据库中获取实时更新。

class WeatherForecastDataStreamViewModel @Inject constructor(
    weatherForecastRepository: WeatherForecastRepository
) : ViewModel() 

    private val _weatherForecast = weatherForecastRepository
        .fetchWeatherForecastRealTime()
        .map 
            // Do some heavy operation. This operation will be done in the
            // scope of this flow collected. In our case it is the scope
            // passed to asLiveData extension function
            // This operation will not block the UI
            delay(1000)
            it
        
        .asLiveData(
            // Use Default dispatcher for CPU intensive work and
            // viewModel scope for auto cancellation when viewModel
            // is destroyed
            Dispatchers.Default + viewModelScope.coroutineContext
        )

    val weatherForecast: LiveData<Result<Int>>
        get() = _weatherForecast

当你获取实时天气预报数据时,map函数中的所有数据转换将在Flow collect的scope内以异步方式完成。

注意:如果你在资源库中没有使用Flow,你可以通过使用liveData builder实现同样的数据转换功能。

private val _weatherForecast = liveData 
    val response = weatherForecastRepository.fetchWeatherForecast()
    
    // Do some heavy operation with response
    delay(1000)
    
    emit(transformedResponse)

再次回到Flow的实时数据获取,我们可以看到它在观察数据流的同时更新文本字段,并没有阻塞UI。

class WeatherForecastDataStreamFragment : DaggerFragment() 
    
    ...
    
    override fun onActivityCreated(savedInstanceState: Bundle?) 
        super.onActivityCreated(savedInstanceState)

        // Obtain viewModel
        viewModel = ViewModelProviders.of(
            this,
            viewModelFactory
        ).get(WeatherForecastDataStreamViewModel::class.java)

        // Observe weather forecast data stream
        viewModel.weatherForecast.observe(viewLifecycleOwner, Observer 
            when (it) 
                Result.Loading -> 
                    Toast.makeText(context, "Loading", Toast.LENGTH_SHORT).show()
                
                is Result.Success -> 
                    // Update weather data
                    tvDegree.text = it.data.toString()
                
                Result.Error -> 
                    Toast.makeText(context, "Error", Toast.LENGTH_SHORT).show()
                
            
        )

        lifecycleScope.launch 
            while (true) 
                delay(1000)
                // Update text 
                tvDegree.text = "Not blocking"
            
        
    

那么它将看起来像这样:

img

用Flow改变你的应用程序的主题

由于Flow可以发出实时更新,我们可以把用户的输入看作是一种更新,并通过Flow发送。为了做到这一点,让我们创建一个主题数据源,它有一个用于广播更新的主题channel。

class ThemeDataSource @Inject constructor(
    private val sharedPreferences: SharedPreferences
) 
    private val themeChannel: ConflatedBroadcastChannel<Theme> by lazy 
        ConflatedBroadcastChannel<Theme>().also  channel ->
            // When there is an access to theme channel
            // get the current theme from shared preferences
            // and send it to consumers
            val theme = sharedPreferences.getString(
                Constants.PREFERENCE_KEY_THEME,
                null
            ) ?: Theme.LIGHT.name // Default theme is light

            channel.offer(Theme.valueOf(theme))
        
    

    @FlowPreview
    fun getTheme(): Flow<Theme> 
        return themeChannel.asFlow()
    

    fun setTheme(theme: Theme) 
        // Save theme to shared preferences
        sharedPreferences
            .edit()
            .putString(Constants.PREFERENCE_KEY_THEME, theme.name)
            .apply()

        // Notify consumers
        themeChannel.offer(theme)
    


// Used to change the theme of the app
enum class Theme 
    DARK, LIGHT

正如你所看到的,没有从外部直接访问themeChannel,themeChannel在被发送之前被转换为Flow。

在Activity层面上消费主题更新是更好的,因为所有来自其他Fragment的更新都可以被安全地观察到。

让我们在ViewModel中获取主题更新。

class MainViewModel @Inject constructor(
    private val themeDataSource: ThemeDataSource
) : ViewModel() 
    // Whenever there is a change in theme, it will be
    // converted to live data
    private val _theme: LiveData<Theme> = themeDataSource
        .getTheme()
        .asLiveData(viewModelScope.coroutineContext)

    val theme: LiveData<Theme>
        get() = _theme

    fun setTheme(theme: Theme) 
        themeDataSource.setTheme(theme)
    

而且在Activity中可以很容易地观察到这一点。

class MainActivity : DaggerAppCompatActivity() 
  
    ...

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        ...

        observeTheme()
    

    private fun observeTheme() 
        // Observe and update app theme if any changes happen
        viewModel.theme.observe(this, Observer  theme ->
            when (theme) 
                Theme.LIGHT -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
                Theme.DARK -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
            
        )
    

剩下的事情就是按下Fragment中的按钮。

class MainFragment : DaggerFragment() 

    private lateinit var viewModel: MainViewModel
  
    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) 
        
        ...

        btnDarkMode.setOnClickListener 
            // Enable dark mode
            viewModel.setTheme(Theme.DARK)
        
    

瞧瞧! 刚刚用Flow改变了主题。

Changing the app theme with using Flow

2

原文链接:https://proandroiddev.com/using-livedata-flow-in-mvvm-part-ii-252ec15cc93a

在第一部分中,我们已经看到了如何在资源库层中使用Flow,以及如何用Flow和LiveData改变应用程序的主题。在这篇文章中,我们将看到如何移除LiveData(甚至是MediatorLiveData),在所有层中只使用Flow。我们还将深入研究常见的Flow操作,如map、filter、transform等。最后,我们将实现一个搜索栏的例子,这个例子是由Sean McQuillan在 "Fragmented Podcast - 187: 与Manuel Vivo和Sean McQuillan的Coroutines "中给出的例子,使用了Channel和Flow。

Say 👋 to LiveData

使用LiveData可以确保在生命周期所有者销毁的情况下,你不会泄露任何资源。如果我告诉你,你几乎可以(后面会解释为什么不一样,但几乎)用Flow获得同样的好处呢?

让我们来看看我们如何做到这一点。

储存库

存储库层保持不变,因为我们已经在返回Flow。

/**
     * This method is used to get data stream of fake weather
     * forecast data in real time with 1000 ms delay
     */
    fun fetchWeatherForecastRealTime() : Flow<Result<Int>> = flow 
        // Fake data stream
        while (true) 
            delay(1000)
            // Send a random fake weather forecast data
            emit(Result.Success((0..20).random()))
        
    

ViewModel

我们不需要用asLiveData将Flow转换为LiveData,而只是在ViewModel中使用Flow。

之前是这样的。

class WeatherForecastDataStreamViewModel @Inject constructor(
    weatherForecastRepository: WeatherForecastRepository
) : ViewModel() 

    private val _weatherForecast = weatherForecastRepository
        .fetchWeatherForecastRealTime()
        .map 
            // Do some heavy operation. This operation will be done in the
            // scope of this flow collected. In our case it is the scope
            // passed to asLiveData extension function
            // This operation will not block the UI
            delay(1000)
            it
        
        .asLiveData(
            // Use Default dispatcher for CPU intensive work and
            // viewModel scope for auto cancellation when viewModel
            // is destroyed
            Dispatchers.Default + viewModelScope.coroutineContext
        )

    val weatherForecast: LiveData<Result<Int>>
        get() = _weatherForecast

只用Flow,它就变成了。

class WeatherForecastDataStreamFlowViewModel @Inject constructor(
    weatherForecastRepository: WeatherForecastRepository
) : ViewModel() 

    private val _weatherForecast = weatherForecastRepository
        .fetchWeatherForecastRealTime()

    val weatherForecast: Flow<Result<Int>>
        get() = _weatherForecast

但是,等等。map过程缺少了,让我们添加它,以便在绘制地图时将摄氏温度转换为华氏温度。

private val _weatherForecast = weatherForecastRepository
    .fetchWeatherForecastRealTime()
    .map 
        // Do some heavy mapping
        delay(500)

        // Let's add an additional mapping to convert
        // celsius degree to Fahrenheit
        if (it is Result.Success) 
            val fahrenheitDegree = convertCelsiusToFahrenheit(it.data)
            Result.Success(fahrenheitDegree)
         else it // Do nothing if result is loading or error
    

/**
 * This function converts given [celsius] to Fahrenheit.
 *
 * Fahrenheit degree = Celsius degree * 9 / 5 + 32
 *
 * @return Fahrenheit integer for [celsius]
 */
private fun convertCelsiusToFahrenheit(celsius: Int) = celsius * 9 / 5 + 32

你可能想在用户界面中显示加载,那么onStart就是一个完美的地方。

private val _weatherForecast = weatherForecastRepository
    .fetchWeatherForecastRealTime()
    .onStart 
        emit(Result.Loading)
    
    .map  ... 

如果你想过滤数值,那就去吧。你有过滤运算符。

private val _weatherForecast = weatherForecastRepository
        .fetchWeatherForecastRealTime()
        .onStart  ... 
        .filter 
            // There could be millions of data when filtering
            // Do some filtering
            delay(2000)

            // Let's add an additional filtering to take only
            // data which is less than 10
            if (it is Result.Success) 
                it.data < 10
             else true // Do nothing if result is loading or error
        
        .map  ... 

你也可以用transform操作符对数据进行转换,这使你可以灵活地对一个单一的值发出你想要的信息。

private val _weatherForecast = weatherForecastRepository
        .fetchWeatherForecastRealTime()
        .onStart  ... 
        .filter  ... 
        .map  ... 
        .transform 
            // Let's send only even numbers
            if (it is Result.Success && it.data % 2 == 0) 
                val evenDegree = it.data
                emit(Result.Success(evenDegree))
              // You can call emit as many as you want in transform
              // This makes transform different from filter operator
             else emit(it) // Do nothing if result is loading or error
        

由于Flow是顺序的,collecting一个值的总执行时间是所有运算符的执行时间之和。如果你有一个长期运行的运算符,你可以使用buffer,这样直到buffer的所有运算符的执行将在一个不同的coroutine中处理,而不是在协程中对Flow collect。这使得总的执行速度更快。

private val _weatherForecast = weatherForecastRepository
        .fetchWeatherForecastRealTime()
        .onStart  ... 
        .filter  ... 
        // onStart and filter will be executed on a different
        // coroutine than this flow is collected
        .buffer()
        // The following map and transform will be executed on the same
        // coroutine which this flow is collected
        .map  ... 
        .transform  ... 

如果你不想多次收集相同的值呢?那么你就可以使用distinctUntilChanged操作符,它只在值与前一个值不同时发送。

private val _weatherForecast = weatherForecastRepository
      .fetchWeatherForecastRealTime()
      .onStart  ... 
      .distinctUntilChanged()
      .filter  ... 
      .buffer()
      .map  ... 
      .transform  ... 

比方说,你只想在显示在用户界面之前缓存修改过的数据。你可以利用onEach操作符来完成每个值的工作。

private val _weatherForecast = weatherForecastRepository
      .fetchWeatherForecastRealTime()
      .onStart  ... 
      .distinctUntilChanged()
      .filter  ... 
      .buffer()
      .map  ... 
      .transform  ... 
      .onEach 
        // Do something with the modified data. For instance
        // save the modified data to cache
        println("$it has been modified and reached until onEach operator")
      

如果你在所有运算符中做一些繁重的工作,你可以通过使用flowOn运算符简单地改变整个运算符的执行环境。

private val _weatherForecast = weatherForecastRepository
      .fetchWeatherForecastRealTime()
      .onStart  ... 
      .distinctUntilChanged()
      .filter  ... 
      .buffer()
      .map  ... 
      .transform  ... 
      .onEach  ... 
      .flowOn(Dispatchers.Default) // Changes the context of flow

错误怎么处理?只需使用catch操作符来捕捉下行流中的任何错误。

private val _weatherForecast = weatherForecastRepository
      .fetchWeatherForecastRealTime()
      .onStart  ... 
      .distinctUntilChanged()
      .filter  ... 
      .buffer()
      .map  ... 
      .transform  ... 
      .onEach  ... 
      .flowOn(Dispatchers.Default)
      .catch  throwable ->
          // Catch exceptions in all down stream flow
          // Any error occurs after this catch operator
          // will not be caught here
          println(throwable)
      

如果我们有另一个流要与_weatherForecast流合并呢?(你可能会认为这是一个有多个LiveData源的MediatorLiveData)你可以使用合并函数来合并任何数量的流量。

private val _weatherForecast = weatherForecastRepository
      .fetchWeatherForecastRealTime()
      .onStart  ... 
      .distinctUntilChanged()
      .filter  ... 
      .buffer()
      .map  ... 
      .transform  ... 
      .onEach  ... 
      .flowOn(Dispatchers.Default)
      .catch  ... 

private val _weatherForecastOtherDataSource = weatherForecastRepository
        .fetchWeatherForecastRealTimeOtherDataSource()

// Merge flows when consumer gets
val weatherForecast: Flow<Result<Int>>
    get() = merge(_weatherForecast, _weatherForecastOtherDataSource)

最后,我们的ViewModel看起来像这样。

@ExperimentalCoroutinesApi
class WeatherForecastDataStreamFlowViewModel @Inject constructor(
    weatherForecastRepository: WeatherForecastRepository
) : ViewModel() 

    private val _weatherForecastOtherDataSource = weatherForecastRepository
        .fetchWeatherForecastRealTimeOtherDataSource()

    private val _weatherForecast = weatherForecastRepository
        .fetchWeatherForecastRealTime()
        .onStart 
            emit(Result.Loading)
        
        .distinctUntilChanged()
        .filter 
            // There could be millions of data when filtering
            // Do some filtering
            delay(2000)

            // Let's add an additional filtering to take only
            // data which is less than 10
            if (it is Result.Success) 
                it.data < 10
             else true // Do nothing if result is loading or error
        
        .buffer()
        .map 
            // Do some heavy mapping
            delay(500)

            // Let's add an additional mapping to convert
            // celsius degree to Fahrenheit
            if (it is Result.Success) 
                val fahrenheitDegree = convertCelsiusToFahrenheit(it.data)
                Result.Success(fahrenheitDegree)
             else it // Do nothing if result is loading or error
        
        .transform 
            // Let's send only even numbers
            if (it is Result.Success && it.data % 2 == 0) 
                val evenDegree = it.data
                emit(Result.Success(evenDegree))
             else emit(it) // Do nothing if result is loading or error
        
        .onEach 
            // Do something with the modified data. For instance
            // save the modified data to cache
            println("$it has modified and reached until onEach operator")
        
        .flowOn(Dispatchers.Default) // Changes the context of flow
        .catch  throwable ->
            // Catch exceptions in all down stream flow
            // Any error occurs after this catch operator
            // will not be caught here
            println(throwable)
        

    // Merge flows when consumer gets
    val weatherForecast: Flow<Result<Int>>
        get() = merge(_weatherForecast, _weatherForecastOtherDataSource)

    /**
     * This function converts given [celsius] to Fahrenheit.
     *
     * Fahrenheit degree = Celsius degree * 9 / 5 + 32
     *
     * @return Fahrenheit integer for [celsius]
     */
    private fun convertCelsiusToFahrenheit(celsius: Int) = celsius * 9 / 5 + 32

唯一剩下的就是Fragment中对Flow实现collect。

class WeatherForecastDataStreamFlowFragment : DaggerFragment() 
  
    ...

    override fun onActivityCreated(savedInstanceState: Bundle?) 
        super.onActivityCreated(savedInstanceState)

        // Obtain viewModel
        viewModel = ViewModelProviders.of(
            this,
            viewModelFactory
        ).get(WeatherForecastDataStreamFlowViewModel::class.java)

        // Consume data when fragment is started
        lifecycleScope.launchWhenStarted 
            // Since collect is a suspend function it needs to be called
            // from a coroutine scope
            viewModel.weatherForecast.collect 
                when (it) 
                    Result.Loading -> 
                        Toast.makeText(context, "Loading", Toast.LENGTH_SHORT).show()
                    
                    is Result.Success -> 
                        // Update weather data
                        tvDegree.text = it.data.toString()
                    
                    Result.Error -> 
                        Toast.makeText(context, "Error", Toast.LENGTH_SHORT).show()
                    
                
            
        
    

这些只是部分Flow运算符。你可以从这里找到整个操作符的列表。

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/index.html

注意:移除LiveData会增加配置变化的额外工作。为了保留配置变化,你需要缓存最新的值。你可以从这里查看Dropbox存储库如何处理缓存。

Search bar using Channel and Flow

在这个播客中,Sean McQuillan举了一个例子,说明如何使用Channel和Flow创建一个搜索栏。这个想法是要有一个带有过滤列表的搜索栏。每当用户在搜索栏中输入一些东西时,列表就会被搜索栏中的文本过滤掉。这是通过在channel中保存文本值和观察通过该channel的流量变化来实现的。

为了演示这个例子,让我们有一个城市列表和一个搜索栏。最后,它看起来会是这样的。

img

我们将在Fragment里有一个EditText。每当文本被更新时,我们将把它发送到存储在ViewModel中的channel。

etCity.doAfterTextChanged 
    val key = it.toString()

    // Set loading indicator
    pbLoading.show()

    // Offer the current text to channel
    viewModel.cityFilterChannel.offer(key)

当channel被更新为最新值时,我们将过滤城市并将列表发送给订阅者。

class SearchCityViewModel @Inject constructor() : ViewModel() 
    val cityList = listOf(
        "Los Angeles", "Chicago", "Indianapolis", "Phoenix", "Houston",
        "Denver", "Las Vegas", "Philadelphia", "Portland", "Seattle"
    )

    // Channel to hold the text value inside search box
    val cityFilterChannel = ConflatedBroadcastChannel<String>()

    // Flow which observes channel and sends filtered list
    // whenever there is a update in the channel. This is
    // observed in UI to get filtered result
    val cityFilterFlow: Flow<List<String>> = cityFilterChannel
        .asFlow()
        .map 
            // Filter cities with new value
            val filteredCities = filterCities(it)

            // Do some heavy work
            delay(500)

            // Return the filtered list
            filteredCities
        

    override fun onCleared() 
        super.onCleared()

        // Close the channel when ViewModel is destroyed
        cityFilterChannel.close()
    

    /**
     * This function filters [cityList] if a city contains
     * the given [key]. If key is an empty string then this
     * function does not do any filtering.
     *
     * @param key Key to filter out the list
     *
     * @return List of cities containing the [key]
     */
    private fun filterCities(key: String): List<String> 
        return cityList.filter 
            it.contains(key)
        
    

然后,只需观察Fragment中的变化。

lifecycleScope.launchWhenStarted 
    viewModel.cityFilterFlow.collect  filteredCities ->
        // Hide the progress bar
        pbLoading.hide()

        // Set filtered items
        adapter.setItems(filteredCities)
    

好了,我们刚刚实现了一个使用channel和流👊的搜索和过滤机制。

3

https://proandroiddev.com/using-livedata-flow-in-mvvm-part-iii-8703d305ca73

第三篇文章主要是针对Flow的测试,这篇文章我相信大家在国内几乎用不上,所以,感兴趣的朋友可以自己去看下。

向大家推荐下我的网站 https://xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问

向大家推荐下我的网站 https://xuyisheng.top/  点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问

往期推荐

本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。

< END >

作者:徐宜生

更文不易,点个“三连”支持一下👇

译《异常最佳实践》

...免应用程序崩溃。这篇文章描述了关于处理与创建异常的最佳实践。处理异常合适地使用异常处理代码(try/catch块)。(因为)同样可以通过编程检 查看详情

Android - MVVM 中 ViewModel 状态的最佳实践?

】Android-MVVM中ViewModel状态的最佳实践?【英文标题】:Android-BestPracticesforViewModelStateinMVVM?【发布时间】:2018-11-1911:01:48【问题描述】:我正在使用MVVM模式沿着LiveData(可能是转换)和View和ViewModel之间的DataBinding开发一个Android应... 查看详情

使用 RxSwift 将 UITableViewCell 中的控件绑定到 ViewModel 的最佳实践

】使用RxSwift将UITableViewCell中的控件绑定到ViewModel的最佳实践【英文标题】:BestpracticeforbindingcontrolsinUITableViewCelltoViewModelusingRxSwift【发布时间】:2019-10-2819:52:04【问题描述】:我正在使用MVC迁移现有应用程序,该应用程序大量使... 查看详情

AWS 中的 cloudformation 最佳实践

】AWS中的cloudformation最佳实践【英文标题】:cloudformationbestpracticesinAWS【发布时间】:2015-02-0505:39:49【问题描述】:我们正处于在AWS上运行我们的服务的早期阶段。我们的服务器托管在AWS中,在VPC中,具有私有和公共子网,并且... 查看详情

在 Laravel 中使用 VS Code 中的语法检查的常量的最佳实践是啥?

】在Laravel中使用VSCode中的语法检查的常量的最佳实践是啥?【英文标题】:WhatisthebestpracticetouseConstantsinLaravelthatleveragesyntaxcheckinginVSCode?在Laravel中使用VSCode中的语法检查的常量的最佳实践是什么?【发布时间】:2021-06-0115:37:51【... 查看详情

将类数据存储在随机访问文件中的最佳实践

】将类数据存储在随机访问文件中的最佳实践【英文标题】:Bestpracticeinstoringclassdatainrandomaccessfiles【发布时间】:2015-10-2514:23:18【问题描述】:在C中,数据通常以struct数据类型组织。这保存在随机访问文件中非常方便,因为您... 查看详情

ETL 中的分段:最佳实践?

】ETL中的分段:最佳实践?【英文标题】:StaginginETL:BestPractices?【发布时间】:2014-06-0215:10:12【问题描述】:目前,我使用的架构采用了一些数据源,其中一个在本地暂存,因为它托管在云中。其他的都在本地托管,所以我执行... 查看详情

最佳实践:将密码存储在表中的最安全方法? [关闭]

】最佳实践:将密码存储在表中的最安全方法?[关闭]【英文标题】:Bestpractices:safestmethodtostorepasswordsinatable?[closed]【发布时间】:2010-12-0805:38:29【问题描述】:我正在使用PHP。我曾经使用原生mysql函数password()来存储密码。有人... 查看详情

将应用程序配置保存在 plist 文件中的最佳实践

】将应用程序配置保存在plist文件中的最佳实践【英文标题】:Bestpracticeofkeepingapplicationconfigurationinplistfile【发布时间】:2012-02-2811:18:05【问题描述】:这听起来可能是个新手问题,反正我是iOS开发新手,我想知道将应用程序配... 查看详情

生产环境中的 Elasticsearch 配置和最佳实践

】生产环境中的Elasticsearch配置和最佳实践【英文标题】:Elasticsearchconfigurationandbestpracticesinproduction【发布时间】:2022-01-1405:49:44【问题描述】:我是使用ELK堆栈的新手,我正在处理存储在物理服务器上的10TB,所以如果有关于多... 查看详情

Angular 中的子父级沟通最佳实践

】Angular中的子父级沟通最佳实践【英文标题】:childparentcommunicationbestpracticesinAngular【发布时间】:2019-01-1701:35:50【问题描述】:我正在努力在Angular方面做得更好,我想了解孩子与父母之间沟通的最佳实践。我目前想要使用的应... 查看详情

最佳实践:通过存储在 DB 中的方案在运行时创建 ORM 对象

】最佳实践:通过存储在DB中的方案在运行时创建ORM对象【英文标题】:Bestpractice:CreatingORMobjectsatruntimebyschemestoredinDB【发布时间】:2010-06-0414:42:07【问题描述】:在我当前的项目(大型企业系统)中,当“简单”对象(如表、引... 查看详情

保管 EAV 的最佳实践

...两个表。我想知道人们用来将这些表移动到DataVault2.0模型中的最佳方法是什么。【问题讨论】:【参考方案1】:在处理将数据存储在EAV模型中的RDBMS时,我们只是将这些表制作成多活动卫星,并将实体作为我们存储在HUB中的业务... 查看详情

通过c#代码检索存储在sql表中的Json值的最佳实践

】通过c#代码检索存储在sql表中的Json值的最佳实践【英文标题】:BestpracticetoretrieveJsonvaluestoredinsqltablethroughc#code【发布时间】:2021-11-3003:04:55【问题描述】:我有一个JSON值存储在SQLServer中。我想检索该JSON值并将其绑定到C#属性... 查看详情

在 Android 上同时将数据保存在内存和数据库中的最佳实践

】在Android上同时将数据保存在内存和数据库中的最佳实践【英文标题】:BestpracticeforkeepingdatainmemoryanddatabaseatsametimeonAndroid【发布时间】:2011-04-1010:18:14【问题描述】:我们正在设计一个包含大量数据(“客户”、“产品”、“... 查看详情

Combine + SwiftUI 中的最佳数据绑定实践?

】Combine+SwiftUI中的最佳数据绑定实践?【英文标题】:Bestdata-bindingpracticeinCombine+SwiftUI?【发布时间】:2020-01-1013:08:34【问题描述】:在RxSwift中,很容易将Driver或ViewModel中的Observable绑定到ViewController中的某个观察者(即UILabel)。... 查看详情

go中的错误和异常处理最佳实践

本文已收录​​编程学习笔记​​。涵盖PHP、JavaScript、Linux、Golang、MySQL、Redis和开源工具等等相关内容。错误认识错误在Go中,错误是一种表示程序错误状态。包含了在程序在运行时、编译时的状态信息。一般我们在编写Go代码... 查看详情

PHP 中的会话超时:最佳实践

】PHP中的会话超时:最佳实践【英文标题】:SessiontimeoutsinPHP:bestpractices【发布时间】:2010-11-1704:58:35【问题描述】:session.gc_maxlifetime和session_cache_expire()之间的实际区别是什么?假设我希望用户会话在非活动15分钟后无效(而不... 查看详情