I like to get sensor data (eg. gyroscope) from a fragment using mvvm. So far I got it working but only in a fragment, completely working around the mvvm environment. It doesn't work from the viewmodel file. How can I stream sensor data into LiveData?
This is working code, but bypassing viewmodel:
class RPMFragment : Fragment(), SensorEventListener {
private lateinit var rpmViewModel: RPMViewModel
private lateinit var sensorManager: SensorManager
private lateinit var gyroscope: Sensor
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
rpmViewModel = ViewModelProviders.of(this).get(RPMViewModel::class.java)
val root = inflater.inflate(R.layout.fragment_rpm, container, false)
val textView: TextView = text_rpm
rpmViewModel.text.observe(this, Observer {
textView.text = it
})
this.sensorManager = activity!!.getSystemService(Context.SENSOR_SERVICE) as SensorManager
sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)?.let {
this.gyroscope = it
}
return root
}
override fun onSensorChanged(event: SensorEvent?) {
if (event?.sensor?.type == Sensor.TYPE_GYROSCOPE) {
text_rpm.text = "z axis: " + event.values[2].toString()
}
}
override fun onResume() {
super.onResume();
sensorManager.registerListener(this, this.gyroscope, SensorManager.SENSOR_DELAY_NORMAL);
// repeat that line for each sensor you want to monitor
}
override fun onPause() {
super.onPause();
sensorManager.unregisterListener(this);
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
}
You can write custom LiveData class to do the "heavy lifting" and let your ViewModel just be a simple host:
// viewmodel only serves as a host for livedata
class RPMViewModel(application: Application) : AndroidViewModel(application) {
val rpmLiveData = RPMLiveData()
// inner class just to have access to application
inner class RPMLiveData : LiveData<String>(), SensorEventListener {
private val sensorManager
get() = getApplication<Application>().getSystemService(Context.SENSOR_SERVICE) as SensorManager
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
//unused
}
override fun onSensorChanged(event: SensorEvent) {
postValue("z axis: ${event.values[2]}")
}
override fun onActive() {
sensorManager.let { sm ->
sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE).let {
sm.registerListener(this, it, SensorManager.SENSOR_DELAY_NORMAL)
}
}
}
override fun onInactive() {
sensorManager.unregisterListener(this)
}
}
}
With this ViewModel in place your fragment is now lightweight and only reacts to changes from LiveData:
class RPMFragment : Fragment() {
private val rpmViewModel by lazy {
ViewModelProviders.of(this).get(RPMViewModel::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val root = inflater.inflate(R.layout.fragment_rpm, container, false)
rpmViewModel.rpmLiveData.observe(viewLifecycleOwner, Observer {
text_rpm.text = it
})
return root
}
}
You have to make your ViewModel extend AndroidViewModel to get access to the current Context. Then let your ViewModel implement the SensorEventListener Interface to get informed about sensor events. Post the event data to your LiveData:
class RpmViewModel(application: Application) : AndroidViewModel(application), SensorEventListener {
private lateinit var sensorManager: SensorManager
private lateinit var gyroscope: Sensor
val sensorDataLiveData = MutableLiveData<String>()
fun registerSensors() {
sensorManager = getApplication<Hero2Application>().getSystemService(Context.SENSOR_SERVICE) as SensorManager
sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)?.let {
this.gyroscope = it
}
sensorManager.registerListener(this, this.gyroscope, SensorManager.SENSOR_DELAY_NORMAL);
}
fun unregisterSensors() {
sensorManager.unregisterListener(this)
}
override fun onSensorChanged(event: SensorEvent?) {
if (event?.sensor?.type == Sensor.TYPE_GYROSCOPE) {
sensorDataLiveData.postValue("z axis: " + event.values[2].toString())
}
}
override fun onAccuracyChanged(p0: Sensor?, p1: Int) {
}
}
In your Fragment you could then listen to the Event like this:
viewModel.sensorDataLiveData.observe(this, Observer {
Log.d("RPMFragment", it)
})
Register and unregister like you did before:
override fun onResume() {
super.onResume()
viewModel.registerSensors()
}
override fun onPause() {
super.onPause()
viewModel.unregisterSensors()
}
Related
My app contains a Room db of cocktail recipes that it downloads via a Retrofit api call, all of that is working well. To focus in on where my problem lies, my use case is a user adding a cocktail to a list. This is done via a DialogFragment and here the DialogFragment displays, the transaction executes, the DialogFragment goes away and the Room db is updated. The cocktail fragment does not get the update though - if you navigate away and back, the update is visible so I know the transaction worked as expected. One thing I just noticed is if I rotate the device, the update gets picked up as well.
Here is the relevant section of my fragment:
class CocktailDetailFragment : BaseFragment<CocktailDetailViewModel, FragmentCocktailDetailBinding, CocktailDetailRepository>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.cocktail.observe(viewLifecycleOwner, Observer {
when(it){
is Resource.Success -> {
updateCocktail(it.value)
}
}
})
}
private fun updateCocktail(cocktail: Cocktail) {
with(binding){
detailCocktailName.text = cocktail.cocktailName
//...
//this is the piece of functionality i'm expecting the LiveData observer to execute and change the drawable
if(cocktail.numLists > 0) {
detailCocktailListImageView.setImageResource(R.drawable.list_filled)
} else {
detailCocktailListImageView.setImageResource(R.drawable.list_empty)
}
}
}
override fun getViewModel() = CocktailDetailViewModel::class.java
override fun getFragmentBinding(
inflater: LayoutInflater,
container: ViewGroup?
) = FragmentCocktailDetailBinding.inflate(inflater,container,false)
override fun getFragmentRepository(): CocktailDetailRepository {
val dao = GoodCallDatabase(requireContext()).goodCallDao()
return CocktailDetailRepository(dao)
}
}
BaseFragment:
abstract class BaseFragment<VM: BaseViewModel, B: ViewBinding, R: BaseRepository>: Fragment() {
protected lateinit var userPreferences: UserPreferences
protected lateinit var binding: B
protected lateinit var viewModel: VM
protected val remoteDataSource = RemoteDataSource()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
userPreferences = UserPreferences(requireContext())
binding = getFragmentBinding(inflater, container)
val factory = ViewModelFactory(getFragmentRepository())
viewModel = ViewModelProvider(this, factory).get(getViewModel())
return binding.root
}
abstract fun getViewModel() : Class<VM>
abstract fun getFragmentBinding(inflater: LayoutInflater, container: ViewGroup?): B
abstract fun getFragmentRepository(): R
}
ViewModelFactory:
class ViewModelFactory(
private val repository: BaseRepository
): ViewModelProvider.NewInstanceFactory() {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return when{
modelClass.isAssignableFrom(CocktailDetailViewModel::class.java) -> CocktailDetailViewModel(repository as CocktailDetailRepository) as T
else -> throw IllegalArgumentException("ViewModel class not found")
}
}
}
ViewModel:
class CocktailDetailViewModel(
private val repository: CocktailDetailRepository
): BaseViewModel(repository) {
private val _cocktail: MutableLiveData<Resource<Cocktail>> = MutableLiveData()
val cocktail: LiveData<Resource<Cocktail>>
get() = _cocktail
fun getCocktailByCocktailId(cocktailId: Int) = viewModelScope.launch {
_cocktail.value = Resource.Loading
_cocktail.value = repository.getCocktail(cocktailId)
}
}
Repository:
class CocktailDetailRepository(
private val dao: GoodCallDao
):BaseRepository(dao) {
suspend fun getCocktail(cocktailId: Int) = safeApiCall {
dao.getCocktail(cocktailId)
}
}
Safe Api Call (I use this so db/api calls run on IO):
interface SafeApiCall {
suspend fun <T> safeApiCall(
apiCall: suspend () -> T
): Resource<T> {
return withContext(Dispatchers.IO) {
try {
Resource.Success(apiCall.invoke())
} catch (throwable: Throwable) {
when (throwable) {
is HttpException -> {
Resource.Failure(false, throwable.code(), throwable.response()?.errorBody())
}
else -> {
Resource.Failure(true, null, null)
}
}
}
}
}
}
Resource:
sealed class Resource<out T> {
data class Success<out T>(val value: T) : Resource<T>()
data class Failure(
val isNetworkError: Boolean,
val errorCode: Int?,
val errorBody: ResponseBody?
): Resource<Nothing>()
object Loading: Resource<Nothing>()
}
Dao:
#Query("SELECT * FROM cocktail c WHERE c.cocktail_id = :cocktailId")
suspend fun getCocktail(cocktailId: Int): Cocktail
Thank you in advance for any help! Given the issue and how the app is working, I believe I have provided all the relevant parts but please advise if more of my code is required to figure this out.
Hey I want to create BaseFragment class that gets viewModel by generic type:
abstract class BaseFragment<B : ViewDataBinding, VM : ViewModel> : DaggerFragment() {
val viewModel by viewModels<VM> { viewModelFactory }
...
}
// Native function
#MainThread
inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)
but getting error Cannot use 'VM' as reified type parameter. Use a class instead.
is it at all possible to achieve what I am trying to do? Maybe with other approach?
Found working way, but is it clean enough?
abstract class BaseModelFragment<VM : ViewModel>(viewModelClass: KClass<VM>) : DaggerFragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
val viewModel by viewModel(viewModelClass) { viewModelFactory }
private fun Fragment.viewModel(
clazz: KClass<VM>,
ownerProducer: () -> ViewModelStoreOwner = { this },
factoryProducer: (() -> ViewModelProvider.Factory)? = null,
) = createViewModelLazy(clazz, { ownerProducer().viewModelStore }, factoryProducer)
}
And usage:
open class SomeFragment : BaseModelFragment<CustomerSupportViewModel>(CustomerSupportViewModel::class) {
...
}
It is tested and working. Any ideas how to improve it? :)
There is clearer solution:
abstract class BaseActivity<VM : BaseViewModel> : AppCompatActivity {
protected val viewModel: VM by viewModel(clazz = getViewModelClass())
private fun getViewModelClass(): KClass<VM> = (
(javaClass.genericSuperclass as ParameterizedType)
.actualTypeArguments[0] as Class<VM>
).kotlin
}
And usage:
class MainActivity : BaseActivity<MainViewModel>(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.onViewCreated()
}
}
There dirty way to get ViewModel and ViewBinding only from generics:
abstract class BaseFragment<BINDING : ViewDataBinding, VM : ViewModel> : Fragment() {
val viewModel by viewModels(getGenericClassAt<VM>(1))
var binding: BINDING? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = inflater.inflateBindingByType<BINDING>(container, getGenericClassAt(0)).apply {
lifecycleOwner = this#BaseFragment
}.also {
it.onBindingCreated()
}
return binding?.root
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = false
internal open fun BINDING.onBindingCreated() {}
fun <T> withBinding(action: BINDING.() -> T): T? = binding?.let { action(it) }
}
#Suppress("UNCHECKED_CAST")
fun <CLASS : Any> Any.getGenericClassAt(position: Int): KClass<CLASS> =
((javaClass.genericSuperclass as? ParameterizedType)
?.actualTypeArguments?.getOrNull(position) as? Class<CLASS>)
?.kotlin
?: throw IllegalStateException("Can not find class from generic argument")
fun <BINDING : ViewBinding> LayoutInflater.inflateBindingByType(
container: ViewGroup?,
genericClassAt: KClass<BINDING>
): BINDING = try {
#Suppress("UNCHECKED_CAST")
genericClassAt.java.methods.first { inflateFun ->
inflateFun.parameterTypes.size == 3
&& inflateFun.parameterTypes.getOrNull(0) == LayoutInflater::class.java
&& inflateFun.parameterTypes.getOrNull(1) == ViewGroup::class.java
&& inflateFun.parameterTypes.getOrNull(2) == Boolean::class.java
}.invoke(null, this, container, false) as BINDING
} catch (exception: Exception) {
throw IllegalStateException("Can not inflate binding from generic")
}
And usage:
class BoardFragment : BaseFragment<FragmentBoardBinding, BoardViewModel>() {
override fun FragmentBoardBinding.onBindingCreated() {
viewModel = this#BoardFragment.viewModel
}
}
Dirty, but saves tones of coding
MyActivity is...
private var preso: ClientResultPresentationFragment? = null
private var presoHelper: PresentationHelper? = null
private val presoListener = object: PresentationHelper.Listener{
override fun initSecondDisplay(display: Display?) {
Log.d("preso", "initSecondDisplay()")
preso = MytPresentationFragment.newInstance(this#MyActivity, display)
preso!!.show(fragmentManager, PRESO) // PRESO is a static value.
}
override fun clearPreso(switchToInline: Boolean) {
if (preso != null) {
preso!!.dismiss()
preso = null
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
// Must open second display at the same time.
onCreatePreso()
refinedResultData = intent.extras!!.getParcelable(RESULT_DATA)
Log.d("result", "${resultData.toString()}")
init(resultData!!)
}
private fun init(data: ResultData){
initView(data)
preso!!.syncData(data)
}
private initView(data: ResultData){
// TODO: initViews...
}
private fun onCreatePreso(){
presoHelper = PresentationHelper(this, presoListener)
presoHelper!!.enable()
}
override fun onResume() {
super.onResume()
presoHelper?.onResume()
}
override fun onPause() {
presoHelper?.onPause()
super.onPause()
}
My Presentation Fragment is...
var mView: View? = null
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Log.d("preso", "onCreateView()")
mView = LayoutInflater.from(context).inflate(R.layout.fragment_my, container, false)
return mView
}
fun syncData(data: ResultData){
Log.d("preso", "syncData()->${data.toString()}")
initView()
// return >>> here...
mView!!.tv_title.text = "${data.title}" // <<< crash here >>>
// TODO: set initial data
}
fun initView(){
// initViews...
}
companion object {
fun newInstance(context: Context?, display: Display?): MyPresentationFragment {
val frag = MyPresentationFragment()
frag.setDisplay(context, display)
return frag
}
}
And the log is...
"resultData.toString()"
initSecondDisplay()
"syncData()->${data.toString()}"
crash------------------
if I just uncomment return part in syncData() of Presentation Fragment(It will not access the views)
"resultData.toString()"
initSecondDisplay()
"syncData()->${data.toString()}"
onCreateView()
So, syncData() is called earlier than onCreateView(). What should I do? I moved preso!!.syncData() after preso!!.show(fragmentManager, PRESO) and it's the same.
Since FragmentManager operations is async, you should send a callback from your fragment to the activity in the fragment OnViewCreated or maybe OnResume and only then send the data from the activity to the fragment.
Or you can just use ViewModel and LiveData to provide your data to fragments
I have a TimerService that I'd like to bind to with a TimerFragment, so I can call the Service's methods and observe its LiveData in the fragment. The problem I'm running into is that when I start my fragment, it's telling me that the lateinit var timerService hasn't been initialized by the time I try to use it in onStart. I thought I was starting the service and binding to it by that time in the fragment lifecycle, so I'm not sure what's causing the issue.
My fragment code is as follows:
class TimerFragment : Fragment() {
private lateinit var scaleUp: ObjectAnimator
private lateinit var alphaDown: ObjectAnimator
private lateinit var timerService: TimerService
private var bound = false
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as TimerService.LocalBinder
timerService = binder.getService()
bound = true
}
override fun onServiceDisconnected(name: ComponentName?) {
bound = false
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
requireActivity().startService(Intent(requireContext(), TimerService::class.java))
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_timer, container, false)
}
override fun onStart() {
super.onStart()
Intent(requireContext(), TimerService::class.java).also{ intent ->
requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
timerService.secondsElapsed.observe(viewLifecycleOwner, Observer {
updateTimerUI(it)
})
timerService.timerState.observe(viewLifecycleOwner, Observer {
updateButtons(it)
})
}
override fun onStop() {
super.onStop()
requireActivity().unbindService(connection)
bound = false
}
// other stuff
}
I am using live data from a shared ViewModel across multiple fragments. I have a sign-in fragment which takes user's phone number and password and then the user presses sign in button I am calling the API for that, now if the sign-in fails I am showing a toast "Sign In failed", now if the user goes to "ForgotPassword" screen which also uses the same view model as "SignInFragment" and presses back from the forgot password screen, it comes to sign-in fragment, but it again shows the toast "Sign In failed" but the API is not called, it gets data from the previously registered observer, so is there any way to fix this?
SignInFragment.kt
class SignInFragment : Fragment() {
private lateinit var binding: FragmentSignInBinding
//Shared view model across two fragments
private val onBoardViewModel by activityViewModels<OnBoardViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_sign_in,
container,
false
)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
//This is calling again after coming back from new fragment it.
showToast("Sign In Failed")
}
}
override fun onClick(v: View?) {
when (v?.id!!) {
R.id.forgotPasswordTV -> {
findNavController().navigate(SignInFragmentDirections.actionSignInFragmentToForgotPasswordFragment())
}
R.id.signInTV -> {
val phoneNumber = binding.phoneNumberET.text
val password = binding.passwordET.text
val signInRequestModel = SignInRequestModel(
phoneNumber.toString(),
password.toString(),
""
)
//Calling API for the sign-in
onBoardViewModel.callSignInAPI(signInRequestModel)
}
}
}
}
ForgotPasswordFragment
class ForgotPasswordFragment : Fragment() {
private lateinit var binding: FragmentForgotPasswordBinding
//Shared view model across two fragments
private val onBoardViewModel by activityViewModels<OnBoardViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_forgot_password,
container,
false
)
return binding.root
}
}
OnBoardViewModel
class OnBoardViewModel : ViewModel() {
private var repository: OnBoardRepository = OnBoardRepository.getInstance()
private val signInRequestLiveData = MutableLiveData<SignInRequestModel>()
//Observing this data in sign in fragment
val signInResponse: LiveData<APIResource<SignInResponse>> =
signInRequestLiveData.switchMap {
repository.callSignInAPI(it)
}
//Calling this function from sign in fragment
fun callSignInAPI(signInRequestModel: SignInRequestModel) {
signInRequestLiveData.value = signInRequestModel
}
override fun onCleared() {
super.onCleared()
repository.clearRepo()
}
}
I have tried to move this code inside onActivityCreated but it's still getting called after coming back from new fragment.
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
showToast("Sign In Failed")
}
Using SingleLiveEvent class instead of LiveData in OnBoardViewModel class will solve your problem:
val signInResponse: SingleLiveEvent <APIResource<SignInResponse>>.
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
fun call() {
postValue(null)
}
}
This is a lifecycle-aware observable that sends only new updates after subscription. This LiveData only calls the observable if there's an explicit call to setValue() or call().
I would provide a way to reset your live data. Give it a nullable type. Your observers can ignore it when they get a null value. Call this function when you receive login data, so you also won't be repeating messages on a screen rotation.
class OnBoardViewModel : ViewModel() {
// ...
fun consumeSignInResponse() {
signInRequestLiveData.value = null
}
}
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
if (response != null) {
showToast("Sign In Failed")
onBoardViewModel.consumeSignInResponse()
}
}
For Kotlin users #Sergey answer can also be implemented using delegates like below
class SingleLiveEvent<T> : MutableLiveData<T>() {
var curUser: Boolean by Delegates.vetoable(false) { property, oldValue, newValue ->
newValue != oldValue
}
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (curUser) {
observer.onChanged(t)
curUser = false
}
})
}
override fun setValue(t: T?) {
curUser = true
super.setValue(t)
}
fun call() {
postValue(null)
}
}