Initializing LiveData from lateinit object - android

I have the following code
lateinit var roomdb:RoomDb
val penLiveData:LiveData<Pen> = roomdb.getPen()
val paperLiveData:LiveData<Paper> = roomdb.getPaper()
val rulerLiveData:LiveData<Ruler> = roomdb.getRuler()
...
As you might expect I am getting an exception that roomdb was not initialized. How do I fix this without too much complication? And please in your response take into account that I have multiple liveData fields that depend on roomdb and I don't want to have to do a bunch of work for each one of them.
roomdb is necessarily initialized by a method call, not inside init.

There is a way to do it with `Transformations.switchMap.
It feels a bit like someone just asked why the gun pointing at their foot doesn't fire. This is me telling you that the safety is still on, what you do to your foot is your business.
Basically, you have a LiveData that will kick of the other's to fetch/refresh.
private val _loadData = MutableLiveData<Boolean>()
val penLiveData = Transformations.switchMap(_loadData) { roomdb.getPen() }
//...
Then you set _loadData.value = true after ensuring that roomdb is set.
Maybe the following is a little cleaner?
private val _loadData = MutableLiveData<RoomDb>()
val penLiveData = Transformations.switchMap(_loadData) { it?.getPen() }
//...
and
_loadData.value = roomdb
or, if within an asynchronous context.
_loadData.postValue(roomdb)
You probably want to return some Default or NullObject (like AbsentLiveData from AAC samples) if switchMap parameter is null.

Related

android compose: remember failed with destructuring pattern

I'm trying to get specific behavior with focus and so use something like this :
val (focusA, focusB) = remember { FocusRequester.createRefs() }
And since i didn't get the correct behavior, start to investigate and the destructuring pattern with remember is the problem.
If you try this (this is what is it done under the hood of FocusRequester.createRefs()):
` class MyClass
object MyClassFactory{
operator fun component1() = MyClass()
operator fun component2() = MyClass()
}
fun createRefs() = MyClassFactory
#Composable
private fun ContentBody() {
val (a, b) = remember {
createRefs()
}
Log.d(">>:a", "${a.hashCode()}")
Log.d(">>:b", "${b.hashCode()}")
}
`
You will realise that a and b are new instance each time there is a recomposition.
Does any one have some information about that? Why remember fail with destructuring pattern. We can see many time this pattern (i use it with constraint layout for example), and according to that, it is a complete failure because each time a new instance are created...
What I'm doing wrong? I solved all my problem by using a remember without destructuring.
Thank.

Coroutines and ViewModel best practice for separation of concers

I needed some direction on being able to observe some flow as live data in my ViewModel class.
For example: The ViewModel class has the field userDataFlow below which combines a few streams of Data Flow. I want to be able to extract out the work of that field into a separate class and let all of the inner working take place there and just want to observe the LiveData to the field in the ViewModel. I would need to pass in few things in the Parameter of that class from the ViewModel which the Flow would need in order to work. Not sure if this is a good practice. Basically, let my ViewModel observe the result and pass it along to the View.
val userDataFlow: Flow<List<UserData>> =
combine(
familyChannel.asFlow(),
userRealTimeData.asFlow,
).asLiveData()
}
Sounds like you need a UseCase/Interactor which in short processes data coming from different repositories.
For example suppose you want a list of your friends that live in countries with a COVID-19 infection rate above a certain value:
class GetFriendsInDangerUseCase(
private val friendsRepository: FriendsRepository,
private val countryRepository: CountryRepository)
fun invoke(threshold: Float) = friendsRepository.friendsFlow
.combine(countryRepository.countriesFlow) { friends, countries ->
val dangerousCountries = countries.filter { it.infectionRate >= threshold }
friends.filter { it.country in dangerousCountries }
}
Then use it like this from your VM:
val friendsInDangerFlow = getFriendsInDanger(0.5)

Why do i get "useless cast" when casting MutableLiveData to LiveData?

I have an array of feedback channels because (outside of question scope) in my ViewModel.
Now, I don't want to expose my MutableLiveData to outside my Viewmodel.
So, i make a private list of LiveData objects, but compiler complains of "Useless Cast"
private val _feedbackChannels = Array(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels
get() = _feedbackChannels.map{
#Suppress("USELESS_CAST") // it is not useless as it no longer exposes the mutableLiveData
it as LiveData<*>
}
Why do I get USELESS_CAST warning?
Compiler doesn't realize you're doing it only to force implication of property type.
Just specify type explicitly and you'll be able to drop the cast entirely. You won't even have to use map, a simple toList() will do:
private val _feedbackChannels = Array(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels : List<LiveData<FeedbackEvent>>
get() = _feedbackChannels.toList()
Clearly the compiler doesn't understand the point of the cast. In order to do this in a more explicit way and remove the costly map function, you can just upcast it like this:
private val _feedbackChannels = Array(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels: Array<out LiveData<FeedbackEvent>>
get() = _feedbackChannels
Edit
If you wanted to expose a List specifically (avoid exposing a mutable array) then you should probably just create one in the first place:
private val _feedbackChannels = List(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels: List<out LiveData<FeedbackEvent>>
get() = _feedbackChannels

LiveData type mistmatch in ViewModel

I was following one of the UDACITYs Android Tutorial on LiveData/Room/Persistence and Repository Architecture.
After gluing the codes all together, I came across (what I believe, a very common issue) Type Mismatch exception.
On the course example, a VideosRepository was created with a member videos which is a LiveData:
class VideosRepository(private val database: VideosDatabase) {
/**
* A playlist of videos that can be shown on the screen.
*/
val videos: LiveData<List<Video>> =
Transformations.map(database.videoDao.getVideos()) {
it.asDomainModel()
}
and in the Model, I have a introduce a MutableLiveData of _video
val playlist = videosRepository.videos //works fine
// added by me
private val _video = MutableLiveData<List<Video>>()
val video: LiveData<List<Video>> = _video
When I tried to access the LiveData, this is where I am getting the Type mismatch.
fun sample(){
_video.value = videosRepository.videos //does not work and throws a Type mismatch.
//Required: List<Video> Found: LiveData<List<Video>>
}
And if I try to just stuff all LiveData in the ViewModel (meaning, only the ViewModel will have the LiveData object declarations) and converting all LiveData to just plain List and a function such as
fun getVideos(): List<Video>{
return database.videoDao.getVideo()
}
I would then get Cannot access database on the main thread since it may potentially lock the UI for a long period of time. which I understand clearly. So if that is the case, then LiveData is the only way to do it.
But how can I get away from the Type mismatch.
PS. I understand concepts of OOP as well as Java, but never had the in-depth hands-on experience, so please bear with me.
Input of _video.value is a List<Video> but you assigned videosRepository.videos that is a LiveData<List<Video>>
You have to get List<Video> from LiveData :
_video.value = videosRepository.videos.value
videosRepository.videos's data type is LiveData<List<Video>> but _video.value's data type is List<Video>, so you can't assign like that.
Try:
val video: LiveData<List<Video>> = videosRepository.videos
Then in the view, observe the livedata to do what you want with the data, an example in Fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
viewModel.video.observe(viewLifecycleOwner, Observer {
val data: List<Video> = it
// Do something with the data such as showing it...
})
}
If you really want to have a MutableLiveData in case of modifying it later, use MediatorLiveData:
private val _video = MediatorLiveData<List<Video>>().apply {
addSource(videosRepository.videos) {
value = it
}
}
val video: LiveData<List<Video>> = _video

How to mock Android Lifecycle components?

I am trying to build an Android app following the recommended design structure.
Let's say, there is a UserRepository for handling the users. However, I would like to have certain settings in the app, for example "Show profile picture", "Sort by", etc. I would like to store these settings in a Room database, just like the Users.
According to my understanding, the cleanest way is to have a separate UserRepository and a SettingsRepository. And of course, the Settings should have a sort of Model, let's call it SettingsModel, in order to be able to retrieve the Settings as a Map for example. Note, that this is not a ViewModel, it has nothing to do with the UI.
Then, the UserRepository should implement its own business (handling the users), just like in the example linked above. Besides that, it also should have a dependency of that SettingsModel, so that it can easily retrieve the settings, which affect how the Users should be retrieved.
The SettingsModel needs to turn the "raw database data" into a map, so that I can reach the settings like this: settings.show_profile_pictures and settings.sort_by, etc.
To achieve this, I need to extract the data from the LiveData, which implies, that I need to observe that LiveData, so that I can update the Map, whenever the settings change.
And here comes the problem: the observe() method needs a LifecycleOwner, which I cannot provide in my tests.
1st attempt: mock with Mockito
It would be an instrumented test, becase that way I have access to an Activity, which is needed to retrieve the DAO.
I am trying to #Inject it with Dagger, but the mocking wasn't successful:
class SettingsRepositoryTest {
private lateinit var settingsDao: SettingsDao
#Mock
private lateinit var mockLifecycleOwner: LifecycleOwner
#Before
fun createDb(){
MockitoAnnotations.initMocks(LifecycleOwner::class.java)
val appContext = InstrumentationRegistry.getTargetContext()
val db = Room.inMemoryDatabaseBuilder(appContext, CurrencyConverterDb::class.java).allowMainThreadQueries().build()
settingsDao = db.settingsDao()
}
#Test
fun testSettingsMap() {
val repo = SettingsRepository(settingsDao, mockLifecycleOwner) // throws the exception here
}
}
The exception:
kotlin.UninitializedPropertyAccessException: lateinit property mockLifecycleOwner has not been initialized
at com.helmet91.currencyconverter.repositories.SettingsRepositoryTestInst.testSettingsMap(SettingsRepositoryTestInst.kt:46)
2nd attempt: use Roboelectric to create an AppCompatActivity, which is in fact a LifecycleOwner.
It is not an instrumented test, because Roboelectric doesn't work in the androidTest environment.
The Activity still has to be mocked, however, it throws a NullPointerException. The only way I can think of, is to go through this Exception stack, and mock everything in it, if it's even possible at all. But that sounds insane to me. There has to be a better solution.
class SettingsRepositoryTest {
private lateinit var settingsDao: SettingsDao
private lateinit var activity: AppCompatActivity
#Before
fun createDb(){
val built = Robolectric.buildActivity(MainActivity::class.java) // throws the exception here
val created = built.create()
val controller = created.start()
activity = controller.get() as AppCompatActivity
val db = Room.inMemoryDatabaseBuilder(activity, CurrencyConverterDb::class.java).allowMainThreadQueries().build()
settingsDao = db.settingsDao()
}
#Test
fun testSettingsMap() {
val repo = SettingsRepository(settingsDao, activity)
val settingsMap = repo.getSettings()
val settingsEntity = Settings(1, "show_flags", "1", "bool")
settingsDao.insert(settingsEntity)
assertTrue(settingsMap.show_flags)
}
}
The exception:
java.lang.NullPointerException
at org.robolectric.internal.bytecode.ShadowImpl.extract(ShadowImpl.java:17)
at org.robolectric.shadow.api.Shadow.extract(Shadow.java:25)
at org.robolectric.Shadows.shadowOf(Shadows.java:1215)
at org.robolectric.shadows.CoreShadowsAdapter.getMainLooper(CoreShadowsAdapter.java:23)
at org.robolectric.android.controller.ComponentController.<init>(ComponentController.java:29)
at org.robolectric.android.controller.ComponentController.<init>(ComponentController.java:21)
at org.robolectric.android.controller.ActivityController.<init>(ActivityController.java:33)
at org.robolectric.android.controller.ActivityController.of(ActivityController.java:25)
at org.robolectric.Robolectric.buildActivity(Robolectric.java:97)
at org.robolectric.Robolectric.buildActivity(Robolectric.java:93)
at com.helmet91.currencyconverter.repositories.SettingsRepositoryTest.createDb(SettingsRepositoryTest.kt:29)
Is it really impossible to test anything, that involves Lifecycle components?

Categories

Resources