whenever I click on the title, the app Crashes shows this logcat
I am new to Android
using
dagger hilt,
exoplayer,
this is where i touch got error:
Songfragment
#AndroidEntryPoint
class SongFragments : Fragment(R.layout.fragment_song) {
#Inject
lateinit var glide: RequestManager
private lateinit var mainViewModel: MainViewModel
private val songViewModel: SongViewModel by viewModels()
private var curplayingSong: sound? = null
private var playbackState: PlaybackStateCompat? = null
private var shouldUpdateSeekbar = true
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mainViewModel = ViewModelProvider(requireActivity()).get(MainViewModel::class.java)
subscribeToObservers()
ivPlayPauseDetail.setOnClickListener {
curplayingSong?.let {
mainViewModel.playOrToggleSound(it, true)
}
}
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
setCurPlayerTimeToTextView(progress.toLong())
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
shouldUpdateSeekbar = false
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
seekBar?.let {
mainViewModel.seekTo(it.progress.toLong())
shouldUpdateSeekbar = true
}
}
})
ivSkipPrevious.setOnClickListener {
mainViewModel.skipToPreviousSound()
}
ivSkip.setOnClickListener{
mainViewModel.skipToNextSound()
}
}
private fun updateTitleAndSongImage(sound: sound) {
val title = "${sound.title} - ${sound.subtitle}"
tvSongName.text = title
glide.load(sound.imageUrl).into(ivSongImage)
}
private fun subscribeToObservers() {
mainViewModel.mediaItems.observe(viewLifecycleOwner){
it?.let { result ->
when(result.status){
Status.SUCCESS -> {
result.data?.let { sounds ->
if (curplayingSong == null && sounds.isNotEmpty()) {
curplayingSong = sounds[0]
updateTitleAndSongImage(sounds[0])
}
}
}
else -> Unit
}
}
}
mainViewModel.curPlayingSound.observe(viewLifecycleOwner) {
if(it == null) return#observe
curplayingSong = it.toSong()
updateTitleAndSongImage(curplayingSong!!)
}
mainViewModel.playbackState.observe(viewLifecycleOwner) {
playbackState = it
ivPlayPauseDetail.setImageResource(
if (playbackState?.isPlaying == true) R.drawable.ic_pause else R.drawable.ic_play
)
seekBar.progress = it?.position?.toInt() ?: 0
}
songViewModel.curPlayerPosition.observe(viewLifecycleOwner) {
if (shouldUpdateSeekbar) {
seekBar.progress = it.toInt()
setCurPlayerTimeToTextView(it)
}
}
songViewModel.curSongDuration.observe(viewLifecycleOwner) {
seekBar.max = it.toInt()
val dateFormat = SimpleDateFormat("mm:ss", Locale.getDefault())
tvSongDuration.text = dateFormat.format(it)
}
}
private fun setCurPlayerTimeToTextView(ms: Long) {
val dateFormat = SimpleDateFormat("mm:ss", Locale.getDefault())
tvCurTime.text = dateFormat.format(ms)
}
}
SongviewModel
class SongViewModel #ViewModelInject constructor(
musicServiceConnection: MusicServiceConnection
) : ViewModel() {
private val playbackState = musicServiceConnection.playBackState
private val _curSongDuration = MutableLiveData<Long>()
val curSongDuration: LiveData<Long> = _curSongDuration
private val _curPlayerPosition = MutableLiveData<Long>()
val curPlayerPosition: LiveData<Long> = _curPlayerPosition
init {
updateCurrentplayerPostion()
}
#SuppressLint("NullSafeMutableLiveData")
private fun updateCurrentplayerPostion() {
viewModelScope.launch {
while(true) {
val pos = playbackState.value?.currentPlaybackPosition
if(curPlayerPosition.value != pos){
_curPlayerPosition.postValue(pos)
_curSongDuration.postValue(MusicService.curSoundDuration)
}
delay(UPDATE_PLAYER_POSITION_INTERVAL)
}
}
}
}
logcat:
2022-08-12 19:11:39.122 10888-10983/com.fridayhouse.snoozz E/ion: ioctl c0044901 failed with code -1: Invalid argument
2022-08-12 19:11:39.318 10888-10888/com.fridayhouse.snoozz E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.fridayhouse.snoozz, PID: 10888
java.lang.RuntimeException: Cannot create an instance of class com.fridayhouse.snoozz.ui.viewmodels.SongViewModel
at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.kt:204)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:322)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:304)
at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.kt:175)
at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.kt:203)
at dagger.hilt.android.internal.lifecycle.HiltViewModelFactory.create(HiltViewModelFactory.java:111)
at androidx.lifecycle.ViewModelProvider$Factory.create(ViewModelProvider.kt:83)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:187)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:153)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:53)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:35)
at com.fridayhouse.snoozz.ui.fragments.SongFragments.getSongViewModel(SongFragments.kt:33)
at com.fridayhouse.snoozz.ui.fragments.SongFragments.subscribeToObservers(SongFragments.kt:121)
at com.fridayhouse.snoozz.ui.fragments.SongFragments.onViewCreated(SongFragments.kt:45)
at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3128)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:552)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1890)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1814)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1751)
at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:538)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:233)
at android.os.Looper.loop(Looper.java:334)
at android.app.ActivityThread.main(ActivityThread.java:8396)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:582)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1068)
Caused by: java.lang.InstantiationException: java.lang.Class<com.fridayhouse.snoozz.ui.viewmodels.SongViewModel> has no zero argument constructor
at java.lang.Class.newInstance(Native Method)
at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.kt:202)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:322)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:304)
at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.kt:175)
at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.kt:203)
at dagger.hilt.android.internal.lifecycle.HiltViewModelFactory.create(HiltViewModelFactory.java:111)
at androidx.lifecycle.ViewModelProvider$Factory.create(ViewModelProvider.kt:83)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:187)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:153)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:53)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:35)
at com.fridayhouse.snoozz.ui.fragments.SongFragments.getSongViewModel(SongFragments.kt:33)
at com.fridayhouse.snoozz.ui.fragments.SongFragments.subscribeToObservers(SongFragments.kt:121)
at com.fridayhouse.snoozz.ui.fragments.SongFragments.onViewCreated(SongFragments.kt:45)
at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3128)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:552)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1890)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1814)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1751)
at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:538)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:233)
at android.os.Looper.loop(Looper.java:334)
at android.app.ActivityThread.main(ActivityThread.java:8396)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:582)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1068)
#ViewModelInject has been deprecated. You can check here for more info.
In newer versions, You need to annotate viewmodel class with #AndroidViewModel and constructor with #Inject.
#AndroidViewModel
class SongViewModel #Inject constructor(
musicServiceConnection: MusicServiceConnection
) : ViewModel()
Hope it would solve this issue.
Since you are using Hilt, you shouldn't be using by viewModels(). You should set up your MusicServiceConnection in your Provider, and instead of using private val songViewModel: SongViewModel by viewModels(), use #Inject private lateinit var songViewModel: SongViewModel.
I'm not very familiar with Hilt, so I can't provide more guidance. The below is an explanation of what's going wrong with by viewModels(), and how you would resolve this if you were not using a dependency injection framework.
When you create a ViewModel in your Activity or Fragment, you can supply a factory that is responsible for creating instances of the ViewModel. If you don't supply a factory, then the default factory is used.
The default factory is only capable of creating instances of your ViewModel if your ViewModel constructor's arguments are one of the following:
constructor() Empty constructor (no arguments)
constructor(savedStateHandle: SavedStateHandle)
constructor(application: Application)
constructor(application: Application, savedStateHandle: SavedStateHandle)
I don't think this is documented very well. I had to look at the Jetpack source code to learn it.
Since your ViewModel needs a MusicServiceConnection parameter, you cannot use the default factory. You will need to supply an explicit ViewModelProvider.Factory to the by viewModels() as an argument.
This training has an example of creating a ViewModelProvider.Factory class for a ViewModel that needs a special argument for the constructor. In your case, the solution might look like this:
class SongViewModelFactory(private val musicServiceConnection: MusicServiceConnection) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
require(modelClass.isAssignableFrom(SongViewModel::class.java)) { "Unknown ViewModel class" }
#Suppress("UNCHECKED_CAST")
return SongViewModel(musicServiceConnection) as T
}
}
I don't know where your MusicServiceConnection class comes from, but let's pretend you can simply instantiate it one time for your Activity with an empty constructor. Then you would replace
private val songViewModel: SongViewModel by viewModels()
with
private val songViewModel: SongViewModel by viewModels {
SongViewModelFactory(MusicServiceConnection())
}
Notice that you are passing a lambda function to viewModels instead of directly passing an instance of your factory. This is because the factory is lazily created only one time, even if your Activity is recreated multiple times (like if the user rotates the screen back and forth). The code inside this lambda is only called once when the app navigates to this screen, even if the Activity is recreated for screen rotations or other configuration changes.
Related
I'd love to observe changes of a shared preference. Here is how I Use Kotlin Flow to do it:
Data source.
interface DataSource {
fun bestTime(): Flow<Long>
fun setBestTime(time: Long)
}
class LocalDataSource #Inject constructor(
#ActivityContext context: Context
) : DataSource {
private val preferences = context.getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE)
#ExperimentalCoroutinesApi
override fun bestTime() = callbackFlow {
trySendBlocking(preferences, PREF_KEY_BEST_TIME)
val listener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == PREF_KEY_BEST_TIME) {
trySendBlocking(sharedPreferences, key)
}
}
preferences.registerOnSharedPreferenceChangeListener(listener)
awaitClose { // NEVER CALLED
preferences.unregisterOnSharedPreferenceChangeListener(listener)
}
}
#ExperimentalCoroutinesApi
private fun ProducerScope<Long>.trySendBlocking(
sharedPreferences: SharedPreferences,
key: String?
) {
trySendBlocking(sharedPreferences.getLong(key, 0L))
.onSuccess { }
.onFailure {
Log.e(TAG, "", it)
}
}
override fun setBestTime(time: Long) = preferences.edit {
putLong(PREF_KEY_BEST_TIME, time)
}
companion object {
private const val TAG = "LocalDataSource"
private const val PREFS_FILE_NAME = "PREFS_FILE_NAME"
private const val PREF_KEY_BEST_TIME = "PREF_KEY_BEST_TIME"
}
}
Repository
interface Repository {
fun observeBestTime(): Flow<Long>
fun setBestTime(bestTime: Long)
}
class RepositoryImpl #Inject constructor(
private val dataSource: DataSource
) : Repository {
override fun observeBestTime() = dataSource.bestTime()
override fun setBestTime(bestTime: Long) = dataSource.setBestTime(bestTime)
}
ViewModel
class BestTimeViewModel #Inject constructor(
private val repository: Repository
) : ViewModel() {
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(0L)
val uiState: StateFlow<Long> = _uiState
init {
viewModelScope.launch {
repository.observeBestTime()
.onCompletion { // CALLED WHEN THE SCREEN IS ROTATED OR HOME BUTTON PRESSED
Log.d("myTag", "viewModelScope onCompletion")
}
.collect { bestTime ->
_uiState.value = bestTime
}
}
}
fun setBestTime(time: Long) = repository.setBestTime(time)
}
Fragment.
#AndroidEntryPoint
class MetaDataFragment : Fragment(R.layout.fragment_meta_data) {
#Inject
lateinit var timeFormatter: TimeFormatter
#Inject
lateinit var bestTimeViewModel: BestTimeViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val bestTimeView = view.findViewById<TextView>(R.id.best_time_value)
// Create a new coroutine in the lifecycleScope
viewLifecycleOwner.lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// This happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
bestTimeViewModel.uiState
.map { millis ->
timeFormatter.format(millis)
}
.onCompletion { // CALLED WHEN THE SCREEN IS ROTATED OR HOME BUTTON PRESSED
Log.d("MyApp", "onCompletion")
}
.collect {
bestTimeView.text = it
}
}
}
}
}
I've noticed that awaitClose is never called. But this is where my clean-up code is. Please advise. If it's not a good idea to use callbackFlow in the first place, please let me know (as you can see some functions are ExperimentalCoroutinesApi meaning their behaviour can change)
I found a solution that allows me to save a simple dataset such as a preference and observe its changes using Kotlin Flow. It's Preferences DataStore.
This is the code lab and guide I used:
https://developer.android.com/codelabs/android-preferences-datastore#0
https://developer.android.com/topic/libraries/architecture/datastore
and this is my code:
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException
data class UserPreferences(val bestTime: Long)
private const val USER_PREFERENCES_NAME = "user_preferences"
private val Context.dataStore by preferencesDataStore(
name = USER_PREFERENCES_NAME
)
interface DataSource {
fun userPreferencesFlow(): Flow<UserPreferences>
suspend fun updateBestTime(newBestTime: Long)
}
class LocalDataSource(
#ApplicationContext private val context: Context,
) : DataSource {
override fun userPreferencesFlow(): Flow<UserPreferences> =
context.dataStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val bestTime = preferences[PreferencesKeys.BEST_TIME] ?: 0L
UserPreferences(bestTime)
}
override suspend fun updateBestTime(newBestTime: Long) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.BEST_TIME] = newBestTime
}
}
}
private object PreferencesKeys {
val BEST_TIME = longPreferencesKey("BEST_TIME")
}
and the dependency to add to build.gradle:
implementation "androidx.datastore:datastore-preferences:1.0.0"
The problem is, that you are injecting your ViewModel as if it was just a regular class, by using
#Inject
lateinit var bestTimeViewModel: BestTimeViewModel
Because of this, the ViewModel's viewModelScope is never cancelled, and therefor the Flow is collected forever.
Per Documentation, you should use
privat val bestTimeViewModel: BestTimeViewModel by viewModels()
This ensures that the ViewModel's onCleared method, which in turn will cancel the viewModelScope, is called when your Fragment is destroyed.
Also make sure your ViewModel is annotated with #HiltViewModel:
#HiltViewModel
class BestTimeViewModel #Inject constructor(...) : ViewModel()
I'm trying to run android instrumentation tests for the fragments that share the same ViewModel. The ViewModel have some arguments.
I use koin, but when using activityViewModels in the tests koin does not inject the ViewModel.
class SomeFragment() : Fragment() {
private val viewModel: SomeViewModel by activityViewModels()
... more code
}
class SomeFragmentTest() : KoinTest{
#Before
fun setup() {
val viewModel: SomeViewModel = mockk(relaxed = true)
startKoin { loadKoinModules(listOf(
module(override = true) { viewModel },
module(override = true) { factory { appAnalytics } })) }
}
.... more code
}
The message received is
Caused by: java.lang.InstantiationException: java.lang.Class<SomeViewModel> has no zero argument constructor
I found a solution, and I posted this question and I hope it helps others.
I added a view model factory this to the fragment:
class SomeFragment(val factory: (() -> ViewModelProvider.Factory)? = null) : Fragment() {
private val viewModel: SomeViewModel by activityViewModels(factory)
... more code
}
class SomeFragmentTest() : KoinTest{
private val fragmentFactory : FragmentFactory = mockk()
#Before
fun setup() {
val viewModel: SomeViewModel = mockk(relaxed = true)
... some mock related to viewModel
val viewModelFactory : ViewModelProvider.Factory = mockk()
every { viewModelFactory.create(SomeViewModel::class.java) } answers { viewModel }
every { fragmentFactory.instantiate(any(), any()) } answers { SomeFragment{
viewModelFactory
}}
}
#Test
fun test_fragment() {
launchFragmentInContainer<SomeFragment>(
themeResId = R.style.AppTheme,
factory = fragmentFactory
)
... some asserts
}
.... more code
}
This works for prod because in the activityViewModels implementation when the factory is null the default is used:
inline fun <reified VM : ViewModel> Fragment.activityViewModels(
noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { requireActivity().viewModelStore },
factoryProducer ?: { requireActivity().defaultViewModelProviderFactory })
I am using Android databinding to listen to live data changes and I would like to observe changes on the viewmodel level (Rather then observing on fragment and then sending a callback to the viewmodel)
The observerForever is interesting as it serves the purpose for me. However when I run a test I get the following error:
java.lang.NullPointerException
at androidx.arch.core.executor.DefaultTaskExecutor.isMainThread(DefaultTaskExecutor.java:77)
at androidx.arch.core.executor.ArchTaskExecutor.isMainThread(ArchTaskExecutor.java:116)
at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:461)
at androidx.lifecycle.LiveData.observeForever(LiveData.java:222)
at com.bcgdv.ber.maha.login.ui.LoginViewModel.<init>(LoginViewModel.kt:43)
at com.bcgdv.ber.maha.login.ui.LoginViewModelTest.<init>(LoginViewModelTest.kt:26)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at org.junit.platform.commons.util.ReflectionUtils.newInstance(ReflectionUtils.java:443)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:60)
My code is as follows in the viewmodel class:
val observerEmail: Observer<String> = Observer {
setEmailError(it)
checkLoginButton()
}
var email = MutableLiveData<String>()
init {
email.observeForever(observerEmail)
}
Also to note is I am using Junit5.
#ExtendWith(InstantTaskExecutorExtension::class)
class LoginViewModelTest {
val emailAddress = "xyz#xyz.com"
val password = "password"
val user: User = User("1", "xyz#xyz.com", "password")
val loginUsecase: LoginUseCase = mock {
on { loginUser(emailAddress, password) } doReturn (Single.just(user))
}
private val loginViewModel: LoginViewModel = LoginViewModel(
loginUsecase,
LoginCredentialsValidator(),
Schedulers.trampoline(),
Schedulers.trampoline()
)
#Test
fun should_return_user_as_null_initially() {
whenever(loginUsecase.getUser()).thenReturn(null)
loginViewModel.init()
assertEquals(
expected = null,
actual = loginViewModel.obsEmail.get()
)
}}
And this is the InstantTaskExecutorExtension.
class InstantTaskExecutorExtension : BeforeEachCallback, AfterEachCallback {
override fun beforeEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance()
.setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
override fun postToMainThread(runnable: Runnable) = runnable.run()
override fun isMainThread(): Boolean = true
})
}
override fun afterEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(null)
}
}
In general it's recommended to use LiveData only for View Model <-> View communication, however I think the issue is:
private val loginViewModel: LoginViewModel = LoginViewModel(
...
)
Because since this is a member variable it would be executed before the test and it's already implicitly executing init() since you call the constructor.
No need to call init() explicitly. I'd remove the loginViewModel member variable and instantiate it in the test function via the constructor:
#Test
fun should_return_user_as_null_initially() {
...
LoginViewModel(
...
)
...
}
I develop app with MVVM pattern. I want save UI when user rotate screen.
MyViewModel.kt
class MyViewModel(val repository: SomeRepository,
state : SavedStateHandle) : ViewModel() {
private val savedStateHandle = state
companion object {
const val KEY = "KEY"
}
fun saveCityId(cityId: String) {
savedStateHandle.set(CITY_KEY, cityId)
}
fun getCityId(): String? {
return savedStateHandle.get(CITY_KEY)
}
}
ViewModelFactory.kt
#Suppress("UNCHECKED_CAST")
class ViewModelFactory(
private val repository: SomeRepository,
private val state: SavedStateHandle
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MyViewModel(repository,state) as T
}
}
I call it in MainActivity
MainActivity.kt
class MainActivity: AppCompatActivity(), KodeinAware {
private val factory: ViewModelFactoryby instance()
override val kodein by kodein()
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
cityId = intent.getStringExtra("cityId") ?: viewModel.getCityId()
if (cityId != null) {
viewModel.saveCityId(cityId!!)
viewModel.getCurrentWeather(cityId!!)
}
}
Here i inject dependencies
Application.kt
class ForecastApplication: Application(), KodeinAware {
override val kodein = Kodein.lazy {
import(androidXModule(this#ForecastApplication))
bind<SomeApi>() with singleton {
Retrofit.create()
}
bind<WeatherRepository>() with singleton {
WeatherRepository(instance())
}
bind() from provider {
WeatherViewModelFactory(
instance(), instance()
)
}
}
}
And i have this error
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.simpleforecast/com.example.simpleapp.UI.Cities.Activity}:org.kodein.di.Kodein$NotFoundException: No binding found for bind<SavedStateHandle>()
with ?<Activity>().? { ? }
How shoud i build ViewModelFactory and inject Saved State module for ViewModel?
SavedStateHandle is parameter which cannot be bound to the DI graph, because it's retrieved from Fragment (or Activity), therefore you need to do several steps in order to make it work:
1) DI viewmodel definition - since you have custom parameter, you need to use from factory:
bind() from factory { handle: SavedStateHandle ->
WeatherViewModel(
state = handle,
repository = instance()
)
}
2) ViewModel Factory - you need to inherit from AbstractSavedStateViewModelFactory
val vmFactory = object : AbstractSavedStateViewModelFactory(this, arguments) {
override fun <T : ViewModel> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
val vmFactory: ((SavedStateHandle) -> WeatherViewModel) = kodein.direct.factory()
return vmFactory(handle) as T
}
}
Inside of the create method you'd retrieve the factory from your DI graph (from step 1).
3) You retrieve ViewModel with the specified factory:
lateinit var vm : WeatherViewModel
fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vm = ViewModelProvider(this, vmFactory)[WeatherViewModel::class.java]
}
or android KTX way:
val vm : WeatherViewModel by viewModels { vmFactory }
I am trying out Kotlin Coroutines and Flow for the first time and I am trying to reproduce a certain flow I use on Android with RxJava with an MVI-ish approach, but I am having difficulties getting it right and I am essentially stuck at this point.
The RxJava app looks essentially like this:
MainActivityView.kt
object MainActivityView {
sealed class Event {
object OnViewInitialised : Event()
}
data class State(
val renderEvent: RenderEvent = RenderEvent.None
)
sealed class RenderEvent {
object None : RenderEvent()
class DisplayText(val text: String) : RenderEvent()
}
}
MainActivity.kt
MainActivity has an instance of a PublishSubject with a Event type. Ie MainActivityView.Event.OnViewInitialised, MainActivityView.Event.OnError etc. The initial Event is sent in onCreate() via the subjects's .onNext(Event) call.
#MainActivityScope
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var subscriptions: CompositeDisposable
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit var onViewInitialisedSubject: PublishSubject<MainActivityView.Event.OnViewInitialised>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupEvents()
}
override fun onDestroy() {
super.onDestroy()
subscriptions.clear()
}
private fun setupEvents() {
if (subscriptions.size() == 0) {
Observable.mergeArray(
onViewInitialisedSubject
.toFlowable(BackpressureStrategy.BUFFER)
.toObservable()
).observeOn(
Schedulers.io()
).compose(
viewModel()
).observeOn(
AndroidSchedulers.mainThread()
).subscribe(
::render
).addTo(
subscriptions
)
onViewInitialisedSubject
.onNext(
MainActivityView
.Event
.OnViewInitialised
)
}
}
private fun render(state: MainActivityView.State) {
when (state.renderEvent) {
MainActivityView.RenderEvent.None -> Unit
is MainActivityView.RenderEvent.DisplayText -> {
mainActivityTextField.text = state.renderEvent.text
}
}
}
}
MainActivityViewModel.kt
These Event's are then picked up by a MainActivityViewModel class which is invoked by .compose(viewModel()) which then transform the received Event into a sort of a new State via ObservableTransformer<Event, State>. The viewmodel returns a new state with a renderEvent in it, which can then be acted upon in the MainActivity again via render(state: MainActivityView.State)function.
#MainActivityScope
class MainActivityViewModel #Inject constructor(
private var state: MainActivityView.State
) {
operator fun invoke(): ObservableTransformer<MainActivityView.Event, MainActivityView.State> = onEvent
private val onEvent = ObservableTransformer<MainActivityView.Event,
MainActivityView.State> { upstream: Observable<MainActivityView.Event> ->
upstream.publish { shared: Observable<MainActivityView.Event> ->
Observable.mergeArray(
shared.ofType(MainActivityView.Event.OnViewInitialised::class.java)
).compose(
eventToViewState
)
}
}
private val eventToViewState = ObservableTransformer<MainActivityView.Event, MainActivityView.State> { upstream ->
upstream.flatMap { event ->
when (event) {
MainActivityView.Event.OnViewInitialised -> onViewInitialisedEvent()
}
}
}
private fun onViewInitialisedEvent(): Observable<MainActivityView.State> {
val renderEvent = MainActivityView.RenderEvent.DisplayText(text = "hello world")
state = state.copy(renderEvent = renderEvent)
return state.asObservable()
}
}
Could I achieve sort of the same flow with coroutines/Flow/Channels? Possibly a bit simplified even?
EDIT:
I have since found a solution that works for me, I haven't found any issues thus far. However this solution uses ConflatedBroadcastChannel<T> which eventually will be deprecated, it will likely be possible to replace it with (at the time of writing) not yet released SharedFlow api (more on that here.
The way it works is that the Activity and viewmodel shares
a ConflatedBroadcastChannel<MainActivity.Event> which is used to send or offer events from the Activity (or an adapter). The viewmodel reduce the event to a new State which is then emitted. The Activity is collecting on the Flow<State> returned by viewModel.invoke(), and ultimately renders the emitted State.
MainActivityView.kt
object MainActivityView {
sealed class Event {
object OnViewInitialised : Event()
data class OnButtonClicked(val idOfItemClicked: Int) : Event()
}
data class State(
val renderEvent: RenderEvent = RenderEvent.Idle
)
sealed class RenderEvent {
object Idle : RenderEvent()
data class DisplayText(val text: String) : RenderEvent()
}
}
MainActivity.kt
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit eventChannel: ConflatedBroadcastChannel<MainActivityView.Event>
private var isInitialised: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
init()
}
private fun init() {
if (!isInitialised) {
lifecycleScope.launch {
viewModel()
.flowOn(
Dispatchers.IO
).collect(::render)
}
eventChannel
.offer(
MainActivityView.Event.OnViewInitialised
)
isInitialised = true
}
}
private suspend fun render(state: MainActivityView.State): Unit =
when (state.renderEvent) {
MainActivityView.RenderEvent.Idle -> Unit
is MainActivityView.RenderEvent.DisplayText ->
renderDisplayText(text = state.renderEvent.text)
}
private val renderDisplayText(text: String) {
// render text
}
}
MainActivityViewModel.kt
class MainActivityViewModel constructor(
private var state: MainActivityView.State = MainActivityView.State(),
private val eventChannel: ConflatedBroadcastChannel<MainActivityView.Event>,
) {
suspend fun invoke(): Flow<MainActivityView.State> =
eventChannel
.asFlow()
.flatMapLatest { event: MainActivityView.Event ->
reduce(event)
}
private fun reduce(event: MainActivityView.Event): Flow<MainActivityView.State> =
when (event) {
MainActivityView.Event.OnViewInitialised -> onViewInitialisedEvent()
MainActivityView.Event.OnButtonClicked -> onButtonClickedEvent(event.idOfItemClicked)
}
private fun onViewInitialisedEvent(): Flow<MainActivityView.State> = flow
val renderEvent = MainActivityView.RenderEvent.DisplayText(text = "hello world")
state = state.copy(renderEvent = renderEvent)
emit(state)
}
private fun onButtonClickedEvent(idOfItemClicked: Int): Flow<MainActivityView.State> = flow
// do something to handle click
println("item clicked: $idOfItemClicked")
emit(state)
}
}
Similiar questions:
publishsubject-with-kotlin-coroutines-flow
Your MainActivity can look something like this.
#MainActivityScope
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var subscriptions: CompositeDisposable
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit var onViewInitialisedChannel: BroadcastChannel<MainActivityView.Event.OnViewInitialised>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupEvents()
}
override fun onDestroy() {
super.onDestroy()
subscriptions.clear()
}
private fun setupEvents() {
if (subscriptions.size() == 0) {
onViewInitialisedChannel.asFlow()
.buffer()
.flowOn(Dispatchers.IO)
.onEach(::render)
.launchIn(GlobalScope)
onViewInitialisedChannel
.offer(
MainActivityView
.Event
.OnViewInitialised
)
}
}
private fun render(state: MainActivityView.State) {
when (state.renderEvent) {
MainActivityView.RenderEvent.None -> Unit
is MainActivityView.RenderEvent.DisplayText -> {
mainActivityTextField.text = state.renderEvent.text
}
}
}
}
I think what you're looking for is the Flow version of compose and ObservableTransformer and as far as I can tell there isn't one. What you can use instead is the let operator and do something like this:
MainActivity:
yourFlow
.let(viewModel::invoke)
.onEach(::render)
.launchIn(lifecycleScope) // or viewLifecycleOwner.lifecycleScope if you're in a fragment
ViewModel:
operator fun invoke(viewEventFlow: Flow<Event>): Flow<State> = viewEventFlow.flatMapLatest { event ->
when (event) {
Event.OnViewInitialised -> flowOf(onViewInitialisedEvent())
}
}
As far as sharing a flow I would watch these issues:
https://github.com/Kotlin/kotlinx.coroutines/issues/2034
https://github.com/Kotlin/kotlinx.coroutines/issues/2047
Dominic's answer might work for replacing the publish subjects but I think the coroutines team is moving away from BroadcastChannel and intends to deprecate it in the near future.
kotlinx-coroutines-core provides a transform function.
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/transform.html
it isn't quite the same as what we are used to in RxJava but should be usable for achieving the same result.