简体   繁体   中英

How to return value from coroutine in viewmodelScope?

I am using Room and I need to return id to Fragment which is returned when insert() .

However, But I couldn't return the value from viewModelScope.

I saw other similar questions, but the answer was to return LiveData .

But I don't need LiveData . I just want to return values of type Long.

How can I do it?


Repo

class WorkoutListRepository(private val dao: WorkoutDao) {
    @RequiresApi(Build.VERSION_CODES.O)
    suspend fun createDailyLog(part: BodyPart) : Long {
        ...
        return dao.insertDailyLog(data)
    }
}

ViewModel

class WorkoutListViewModel(
    private val repository: WorkoutListRepository
) : ViewModel() {
    
    ...
    
    @RequiresApi(Build.VERSION_CODES.O)
    fun createDailyLog(part: BodyPart) : Long {
        viewModelScope.launch(Dispatchers.IO) {
            return@launch repository.createDailyLog(part) // can't return
        }
    }
}

Fragment

class WorkoutListTabPagerFragment : Fragment(), WorkoutListAdapter.OnItemClickListener {
   ...
 
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        _binding = FragmentWorkoutListTabPagerBinding.inflate(inflater, container, false)

            ...
        return binding.root
    }
    
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onItemClick(workout: String) {
        when(PageState.curPageState) {
            is PageState.startWorkout -> {
                val id = vm.createDailyLog(part)
                    ...
            }
            is PageState.addWorkout -> //TODO:
            is PageState.editWorkout -> //TODO:
        }
    }
}

But I don't need LiveData

You do. You need some kind of observable data holder because the code inside launch is asynchronous. It doesn't run immediately. It is only kind of scheduled for execution. launch function, on the other hand, returns immediately, ie your createDailyLog function in ViewModel returns before the call to repository.createDailyLog(part) is made. So you can't return a value synchronously from an asynchronous method.

You could either use LiveData or Kotlin's StateFlow to send this data to the Fragment. Your fragment will observe changes to that state and respond accordingly. I suggest using StateFlow here. The code will look somewhat like this:

// ViewModel
class WorkoutListViewModel(
    private val repository: WorkoutListRepository
) : ViewModel() {
    

    private val _logIdFlow = MutableStateFlow<Long?>(null)
    val logIdFlow = _logIdFlow.asStateFlow()
    ...
    
    @RequiresApi(Build.VERSION_CODES.O)
    fun createDailyLog(part: BodyPart) : Long {
        viewModelScope.launch(Dispatchers.IO) {
            _logIdFlow.value = repository.createDailyLog(part)
        }
    }
}

// Fragment
override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        _binding = FragmentWorkoutListTabPagerBinding.inflate(inflater, container, false)

        ...

        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.logIdFlow.collect { logId ->
                if(logId != null) {
                    // Do whatever you want with the log Id
                }
            }
        }

        return binding.root
    }

An alternate solution can be to use Kotlin Channel and send data through that Channel .

If you just need a quick, short solution, you can call the repository function from the Fragment's lifecycle scope directly, like this:

// ViewModel
suspend fun createDailyLog(part: BodyPart) : Long {
    return repository.createDailyLog(part)
}

//Fragment
override fun onItemClick(workout: String) {
    viewLifecycleOwner.lifecycleScope.launch {
        when(PageState.curPageState) {
            is PageState.startWorkout -> {
                val id = vm.createDailyLog(part) // This will now work
                    ...
            }
            is PageState.addWorkout -> //TODO:
            is PageState.editWorkout -> //TODO:
        }
    }
}

The only problem with this solution is that, now db operation is tied to fragment's lifecycle. So if there is any event which destroy's fragment's lifecycle (like a config change), the operation will be cancelled. This shouldn't be that big of an issue here as your db operation will only take a few milliseconds. But the first option of using a StateFlow or Channel to send data to Fragment/Activity is a more general and recommended way. You can go with whichever option you like.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM