I have a function for the user's login.
But it is suspended.
I try to get its return value, but I can't.
Here's what I tried to do
Code
class LoginViewModel #ViewModelInject constructor(private val remoteDataSource: OrderRemoteDataSource) :
ViewModel() {
private fun areValidCredentials(username: String?, password: String?): Boolean {
return username != null && password != null && username.length > 4 && password.length > 4
}
suspend fun login(username: String?, password: String?): Boolean {
return suspendCoroutine { it ->
val valid = areValidCredentials(username, password)
if (valid) {
// call finish so login activity won't show up after back button clicked in home fragment
try {
viewModelScope.launch {
//TODO CHECK if error code
val loginResponse =
remoteDataSource.login(LoginRequest(username!!, password!!))
if (loginResponse.status == Resource.Status.SUCCESS) {
val jwtToken = loginResponse.data?.jwtToken
if (!jwtToken.isNullOrEmpty()) {
sessionManager.saveAuthToken(jwtToken!!)
//ERROR!
it.resume(true)
}
}
}
} catch (e: Exception) {
Log.i("[LoginActivity]", e.localizedMessage!!)
it.resume(false)
e.printStackTrace()
}
} else {
Toast.makeText(
LOGIN_ACTIVITY,
"Username and password must be at least 5 letters long",
Toast.LENGTH_SHORT
).show()
}
it.resume(false)
}
}
}
And i call it
#AndroidEntryPoint
class LoginFragment : Fragment() {
private val mViewModel: LoginViewModel by viewModels()
private lateinit var navController: NavController
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.frg_login, container, false)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("[LoginFragment]", "onCreate fun started!")
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = findNavController()
loginButton.setOnClickListener {
//TODO navigate to new fragmnet
lifecycleScope.launch {
mViewModel.login(
loginUsernameText.text.toString(),
loginPasswordText.text.toString()
)
}
}
}
And i have error
E/AndroidRuntime: FATAL EXCEPTION: main
Process: ru.gkomega.navigation, PID: 11863
java.lang.IllegalStateException: Already resumed
at kotlin.coroutines.SafeContinuation.resumeWith(SafeContinuationJvm.kt:45)
at ru.gkomega.maumarket.ui.login.LoginViewModel$login$$inlined$suspendCoroutine$lambda$1.invokeSuspend(LoginViewModel.kt:40)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947) I/chatty:
uid=10163(ru.gkomega.navigation) identical 16 lines W/mega.navigatio:
Got a deoptimization request on un-deoptimizable method
java.lang.Class java.lang.Class.classForName(java.lang.String,
boolean, java.lang.ClassLoader) I/Process: Sending signal. PID: 11863
SIG: 9 Disconnected from the target VM, address: 'localhost:58264',
transport: 'socket'
I don't know much about coroutines so they're probably the problem
You are resuming the coroutine regardless of the request happened or not, it failed or not.
suspend fun login(username: String?, password: String?): Boolean = suspendCoroutine { cont ->
if (areValidCredentials(username, password)) {
try {
viewModelScope.launch {
val loginResponse = remoteDataSource.login(LoginRequest(username!!, password!!))
val jwtToken = loginResponse.data?.jwtToken
if (loginResponse.status == Resource.Status.SUCCESS && !jwtToken.isNullOrEmpty()) {
sessionManager.saveAuthToken(jwtToken!!)
cont.resume(true)
} else cont.resume(false) // <-- Don't forget
}
} catch (e: Exception) {
Log.i("[LoginActivity]", e.localizedMessage!!)
cont.resume(false)
e.printStackTrace()
}
} else {
Toast.makeText(
LOGIN_ACTIVITY,
"Username and password must be at least 5 letters long",
Toast.LENGTH_SHORT
).show()
cont.resume(false) // <-- Put it right here.
}
// cont.resume(false) // <-- Not here
}
Try this code!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = findNavController()
loginButton.setOnClickListener {
//TODO navigate to new fragment
val isAuth = mViewModel.login(
loginUsernameText.text.toString(),
loginPasswordText.text.toString()
)
if (isAuth) {
val startMainAction =
LoginFragmentDirections.actionStartMain(loginUsernameText.text.toString())
navController.navigate(startMainAction)
}
}
}
And this fragment on viewModel
fun login(username: String?, password: String?): Boolean {
var isAuth = false
val valid = areValidCredentials(username, password)
if (valid) {
// call finish so login activity won't show up after back button clicked in home fragment
try {
//TODO CHECK if error code
runBlocking {
val loginResponse = remoteDataSource.login(LoginRequest(username!!, password!!))
if (loginResponse.status == Resource.Status.SUCCESS) {
val jwtToken = loginResponse.data?.jwtToken.toString()
if (!jwtToken.isNullOrEmpty()) {
sessionManager.saveAuthToken(jwtToken)
isAuth = true
}
}
}
} catch (e: Exception) {
Log.i("[LoginActivity]", e.localizedMessage!!)
e.printStackTrace()
}
I'm not sure if this code is correct, but it works!
Related
We have a random crash on production in this class when accessing the binding at line 10 :
class BulletinFragment : Fragment(R.layout.fragment_bulletins) {
private val bulletinViewModel: BulletinsViewModel by viewModel()
private val binding by viewBinding(FragmentBulletinsBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
bulletinViewModel.switchState.collect {
binding.bulletinLiveNotificationsBanner.switch.isSelected = it
}
}
}
}
}
viewModel is provided by Koin, and the binding delegate is Zhuiden's one from here
class FragmentViewBindingDelegate<T : ViewBinding>(
val fragment: Fragment,
val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {
private var _binding: T? = null
init {
fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
_binding = null
}
})
}
}
})
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
val binding = _binding
if (binding != null) {
return binding
}
val lifecycle = fragment.viewLifecycleOwner.lifecycle
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
}
return viewBindingFactory(thisRef.requireView()).also { _binding = it }
}
}
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory)
inline fun <T : ViewBinding> AppCompatActivity.viewBinding(
crossinline bindingInflater: (LayoutInflater) -> T
): Lazy<T> {
return lazy(LazyThreadSafetyMode.NONE) {
bindingInflater.invoke(layoutInflater)
}
}
This fragment is called within a viewPager2:
class CartPagerAdapter(fragment: Fragment) : FragmentStateAdapter(
fragment.childFragmentManager,
fragment.viewLifecycleOwner.lifecycle
) {
val firstFragment = FirstFragment()
val secondFragment = SecondFragment()
override fun createFragment(position: Int): Fragment = when (position) {
Tab.FIRST_TAB.tabIndex -> firstFragment
Tab.SECOND_TAB.tabIndex -> secondFragment
Tab.THIRD_TAB.tabIndex -> BulletinFragment()
else -> error("The fragment position should in 0 < x < 2 but was '$position'")
}
fun handleDeeplink(deeplink: Uri) {
when (deeplink.host) {
FIRST_TAB_DEEPLINK_HOST -> firstFragment.handleDeeplink(deeplink)
SECOND_TAB_DEEPLINK_HOST -> secondFragment.handleDeeplink(deeplink)
}
}
override fun getItemCount(): Int = 3
}
class CartHomeFragment : Fragment(R.layout.fragment_cart_home), CartHomeContract.View {
private var tabLayoutMediator: TabLayoutMediator? = null
private val args: CartHomeFragmentArgs by navArgs()
private var initTab: Int? = null
// betSlip needs to scroll to top when displaying QrCodes tab (set when moving to QrCode tab after validating cart)
private val pagerAdapter: CartPagerAdapter by adapter { CartPagerAdapter(this) }
private val binding by viewBinding(FragmentCartHomeBinding::bind)
private val scope
get() = viewLifecycleOwner.lifecycleScope
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initTab = args.tabIndex
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initialize()
}
override fun onDestroyView() {
tabLayoutMediator?.detach()
tabLayoutMediator = null
super.onDestroyView()
}
private fun initialize() {
scope.launch {
binding.fragmentCartHomeViewPager.run {
adapter = pagerAdapter
setPagerCurrentItem(this)
offscreenPageLimit = 2
}
tabLayoutMediator = TabLayoutMediator(binding.cartTabLayout, binding.fragmentCartHomeViewPager) { tab, position ->
val (title, contentDesc) = with(getTab(position)) { getString(title) to getString(contentDesc) }
tab.text = title
tab.contentDescription = contentDesc
}
tabLayoutMediator?.attach()
}
}
private fun setPagerCurrentItem(viewPager: ViewPager2) {
val initialIntent = arguments?.getParcelable<Intent>(NavController.KEY_DEEP_LINK_INTENT)
initialIntent?.data?.let {
pagerAdapter.handleDeeplink(it)
viewPager.setCurrentItemForDeeplink(it.host)
initialIntent.data = null
} ?: run {
initTab?.let {
viewPager.setCurrentItem(it, false)
initTab = null
}
}
}
fun getTab(index: Int): Tab {
return Tab.values()[index]
}
private fun ViewPager2.setCurrentItemForDeeplink(deeplink: String?) {
setCurrentItem(Tab.getTabIndexForDeeplink(deeplink), false)
}
companion object {
const val FIRST_TAB_DEEPLINK_HOST = "first"
const val SECOND_TAB_DEEPLINK_HOST = "second"
const val THIRD_TAB_DEEPLINK_HOST = "third"
val DEFAULT_TAB: Tab = Tab.FIRST_TAB
const val SECOND_TAB_DEEPLINK_DETAILS_PATH = "/details"
}
}
enum class Tab(#StringRes val title: Int, #StringRes val contentDesc: Int, val deeplink: String) {
FIRST_TAB(R.string.first_tab_tab_title, R.string.a11y_first_tab, FIRST_TAB_DEEPLINK_HOST),
SECOND_TAB(R.string.second_tab_title, R.string.a11y_second_tab, SECOND_TAB_DEEPLINK_HOST),
THIRD_TAB(R.string.third_tab_title, R.string.a11y_third_tab, THIRD_TAB_DEEPLINK_HOST);
val tabIndex: Int = ordinal
companion object {
fun getTabIndexForDeeplink(deeplink: String?): Int =
(values().firstOrNull { it.deeplink == deeplink }
?: DEFAULT_TAB)
.tabIndex
}
}
In the BulletinFragment, I know that the repeatOnLifecycle block seems useless here but we need it for some reason that is not necessary to explain here. I just would like to understand what is wrong with this piece of code. Actually, we get from crashlytics the following crash happening randomly (rare enough to not succeed to reproduce it, but frequent enough to significantly decrease the crashfree):
Fatal Exception: java.lang.IllegalStateException Can't access the Fragment View's LifecycleOwner when getView() is null i.e., before onCreateView() or after onDestroyView()
androidx.fragment.app.Fragment.getViewLifecycleOwner (Fragment.java:377)
com.mycompany.myapp.common.tools.FragmentViewBindingDelegate.getValue (FragmentViewBindingDelegate.kt:40)
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment.<clinit> (BulletinFragment.kt:18)
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment.access$getBinding (BulletinFragment.java:15)
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment$onViewCreated$2$1$1.emit (BulletinFragment.kt:25)
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment$onViewCreated$2$1$1.emit (BulletinFragment.kt:24)
com.mycompany.myapp.domain.usecase.notifications.LiveNotificationsUseCase$getSwitchStateFlow$$inlined$map$1$2.emit (Emitters.kt:227)
com.mycompany.myapp.domain.usecase.notifications.LiveNotificationsUseCase$getSwitchStateFlow$$inlined$map$1$2$1.invokeSuspend (Emitters.kt:12)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.internal.DispatchedContinuation.resumeWith (DispatchedContinuation.kt:205)
kotlin.coroutines.SafeContinuation.resumeWith (SafeContinuationJvm.kt:41)
How can we endup with this crash when
we tie the coroutine with the viewLifecycleOwner lifecycleScope
and the collect is done inside a block where the lifecycleOwner state is STARTED, the lifecycleOwner being the view if I properly understand.
How the view can be null in this case ??? Is it related to the ViewPager2
I m getting data from two End points using flows and assigning those two list to temporary list in ViewModel. For this purpose, I'm using combine function and returning result as stateFlows with stateIn operator but that's not working. Can anyone point me out where I go wrong please.
ViewModel.kt
private val _movieItem: MutableStateFlow<State<List<HomeRecyclerViewItems>>> =
MutableStateFlow(State.Loading())
val movieItems: StateFlow<State<List<HomeRecyclerViewItems>>> = _movieItem
fun getHomeItemList() {
viewModelScope.launch {
val testList: Flow<State<List<HomeRecyclerViewItems.Movie>>> =
settingsRepo.getMovieList().map {
State.fromResource(it)
}
val directorList: Flow<State<List<HomeRecyclerViewItems.Directors>>> =
settingsRepo.getDirectorList().map {
State.fromResource(it)
}
_movieItem.value = combine(testList, directorList) { testList, directorList ->
testList + directorList // This is not working as "+" Unresolve Error
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
State.loading<Nothing>()
) as State<List<HomeRecyclerViewItems>> // Unchecked cast: StateFlow<Any> to State<List<HomeRecyclerViewItems>>
}
Repository.kt
fun getMovieList(): Flow<ResponseAPI<List<HomeRecyclerViewItems.Movie>>> {
return object :
NetworkBoundRepository<List<HomeRecyclerViewItems.Movie>, List<HomeRecyclerViewItems.Movie>>() {
override suspend fun saveRemoteData(response: List<HomeRecyclerViewItems.Movie>) {
}
override fun fetchFromLocal() {
}
override suspend fun fetchFromRemote(): Response<List<HomeRecyclerViewItems.Movie>> =
apiInterface.getMoviesList()
}.asFlow()
}
fun getDirectorList(): Flow<ResponseAPI<List<HomeRecyclerViewItems.Directors>>> {
return object :
NetworkBoundRepository<List<HomeRecyclerViewItems.Directors>, List<HomeRecyclerViewItems.Directors>>() {
override suspend fun saveRemoteData(response: List<HomeRecyclerViewItems.Directors>) {
}
override fun fetchFromLocal() {
}
override suspend fun fetchFromRemote(): Response<List<HomeRecyclerViewItems.Directors>> =
apiInterface.getDirectorsList()
}.asFlow()
}
Network BoundRepository.kt
#ExperimentalCoroutinesApi
abstract class NetworkBoundRepository<RESULT, REQUEST> {
fun asFlow() = flow<ResponseAPI<REQUEST>> {
val apiResponse = fetchFromRemote()
val remotePosts = apiResponse.body()
if (apiResponse.isSuccessful && remotePosts != null) {
emit(ResponseAPI.Success(remotePosts))
} else {
emit(ResponseAPI.Failed(apiResponse.errorBody()!!.string()))
}
}.catch { e ->
e.printStackTrace()
emit(ResponseAPI.Failed("Server Problem! Please try again Later. "))
}
#WorkerThread
protected abstract suspend fun saveRemoteData(response: REQUEST)
#MainThread
protected abstract fun fetchFromLocal()
#MainThread
protected abstract suspend fun fetchFromRemote(): Response<REQUEST>
}
Endpoints with Sealed Class
#GET("directors")
fun getDirectorsList(): Response<List<HomeRecyclerViewItems.Directors>>
#GET("movies")
fun getMoviesList(): Response<List<HomeRecyclerViewItems.Movie>>
sealed class HomeRecyclerViewItems {
class Title(
val id: Int,
val title: String
) : HomeRecyclerViewItems()
class Movie(
val id: Int,
val title: String,
val thumbnail: String,
val releaseDate: String
) : HomeRecyclerViewItems()
class Directors(
val id: Int,
val name: String,
val avator: String,
val movie_count: Int
) : HomeRecyclerViewItems()
}
Fragment.kt
#AndroidEntryPoint
#ExperimentalCoroutinesApi
class SettingsFragment : BaseBottomTabFragment() {
private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!!
private val viewModel by viewModels<SettingViewModel>()
#Inject
lateinit var recyclerViewAdapter: RecyclerViewAdapter
#Inject
lateinit var bundle: Bundle
var finalList = mutableListOf<HomeRecyclerViewItems>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentSettingsBinding.inflate(layoutInflater,container,false)
val view = binding.root
binding.rvMovie.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(activity)
}
bundle.putString("Hello","hihg")
Toast.makeText(activity, "${bundle.getString("Hello")}", Toast.LENGTH_SHORT).show()
finalList.add(HomeRecyclerViewItems.Title(1,"hello"))
return view
}
private fun observeList() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED){
launch {
viewModel.movieItems.collect { state ->
when(state){
is State.Loading ->{
}
is State.Success->{
if (state.data.isNotEmpty()){
recyclerViewAdapter = RecyclerViewAdapter()
binding.rvMovie.adapter = recyclerViewAdapter
recyclerViewAdapter.submitList(finalList)
}
}
is State.Error -> {
Toast.makeText(activity, "Error", Toast.LENGTH_SHORT).show()
}
else -> Unit
}
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
(activity as MainActivity).binding.ivSearch.isGone = true
viewModel.getHomeItemList()
observeList()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Note: I m following this tutorial simpliedCoding for api data for multirecyclerview but want to implement it with Kotlin State Flow. Any help in this regard is highly appreciated. Thanks.
Your problem is in here
val testList: Flow<State<List<HomeRecyclerViewItems.Movie>>> =
settingsRepo.getMovieList().map {
State.fromResource(it)
}
val directorList: Flow<State<List<HomeRecyclerViewItems.Directors>>> =
settingsRepo.getDirectorList().map {
State.fromResource(it)
}
_movieItem.value = combine(testList, directorList) { testList, directorList ->
testList + directorList
}
They are not returning a List<HomeRecyclerViewItems>, but a State<List<HomeRecyclerViewItems>. Maybe a better name for the variables are testsState and directorsState. After that it will be more clear why you need to unpack the values before combining the lists
_movieItem.value = combine(testsState, directorsState) { testsState, directorsState ->
val homeRecyclerViewItems = mutableListOf<HomeRecyclerViewItems>()
if (testsState is Success) homeRecyclerViewItems.add(testsState.data)
if (directorsState is Success) homeRecyclerViewItems.add(directorsState.data)
homeRecyclerViewItems
}
I'm Trying to make a viewmodel scoped to my application to control logic related to showing of not showing pin in multi activity app .
I've used AndroidViewModel to pass the application to it and here is the class for AppViewModel
#HiltViewModel
class AppViewModel #Inject constructor(
private val getUserPassCodeUseCase: GetUserPassCodeUseCase,
private val isPasscodeInputUseCase: IsPasscodeInputUseCase,
private val clearAllDataUseCase: ClearAllDataUseCase,
#ApplicationContext private val context: Context
) : AndroidViewModel((context as App)) {
private val _openPin = MutableSharedFlow<Long>()
val openPin = _openPin.asSharedFlow()
// uptime in millis
private var time: Long = 0
private var restoreStatus = RestoreStatus.EMPTY
private var isPasscode = false
private var passCode = ""
init {
// get user status
}
private fun checkIfShouldLock() {
viewModelScope.launch {
isPasscode = withContext(IO) {
isPasscodeInputUseCase()
}
val userHasAccount = (restoreStatus == RestoreStatus.ID_SUBMISSION
|| restoreStatus == RestoreStatus.TERMS_AND_CONDITION
|| restoreStatus == RestoreStatus.ACTIVATE_CARD
|| restoreStatus == RestoreStatus.FULL_NAME
|| restoreStatus == RestoreStatus.COMPLETED)
if (true)
_openPin.emit (System.currentTimeMillis() )
}
}
fun onResume() {
updatePasscode()
if (!isPasscode) {
time = 0
return
}
val now = SystemClock.elapsedRealtime()
when {
time == 0L -> {
// remember first value
time = now
}
// check is session expired
now - time > sessionExpiredTime -> {
time = now
when (restoreStatus) {
RestoreStatus.COMPLETED -> checkIfShouldLock()
RestoreStatus.EMPTY -> {}
else -> {}
}
}
else -> {
time = now
}
}
}
fun onPause() {
updatePasscode()
viewModelScope.launch {
delay(Constants.PASSCODE_DELAY)
if (!isPasscode) {
time = 0
return#launch
}
// start "timer"
time = SystemClock.elapsedRealtime()
}
}
private fun updatePasscode() {
viewModelScope.launch {
isPasscode = withContext(IO) {
isPasscodeInputUseCase()
}
}
}
fun logout() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
clearAllDataUseCase()
}
}
}
companion object {
const val MAIN_VIEW_MODEL_TAG = "AppViewModel"
}
}
and here is my application class and how i try to access the viewmodel
#HiltAndroidApp
class App : Application(), Application.ActivityLifecycleCallbacks,
Configuration.Provider {
#Inject
lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
// uptime in millis
private var time: Long = 0
var appViewModel : AppViewModel ?=null
var currentActivity : String ?=null
override fun onCreate() {
super.onCreate()
AndroidThreeTen.init(this)
appViewModel = ViewModelProvider.AndroidViewModelFactory(this).create(AppViewModel::class.java)
appViewModel?.openPin?.onEach {
// if (authToken.isNotEmpty())
when (currentActivity) {
SplashActivity::class.java.name,
PinActivity::class.java.name -> Unit
else -> {
startActivity(Intent(this, PinActivity::class.java).apply {
// flag of should end with result or not
// putExtra(Constants.IS_CAME_FROM_BACKGROUND, true)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}
}
}
setupCrashlytics()
if (BuildConfig.DEBUG)
Timber.plant(Timber.DebugTree())
else
Timber.plant(CrashReportingTree())
DyScan.init(this, Constants.DYSCAN_API_KEY)
registerActivityLifecycleCallbacks(this)
}
private fun setupCrashlytics() {
with(FirebaseCrashlytics.getInstance()) {
setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
}
}
private fun isDeviceRooted(): Boolean {
var process: Process? = null
return try {
process = Runtime.getRuntime().exec("su")
true
} catch (e: Exception) {
Timber.i(e, "Rooted device command exception")
false
} finally {
if (process != null) {
try {
process.destroy()
} catch (e: Exception) {
Timber.i(e, "Rooted device command close exception")
}
}
}
}
private fun hideSystemBars(activity: Activity) {
val windowInsetsController =
ViewCompat.getWindowInsetsController(activity.window.decorView) ?: return
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE
windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())
}
override fun onActivityCreated(activity: Activity, p1: Bundle?) {
currentActivity = activity.localClassName
if (isDeviceRooted()) {
Toast.makeText(
activity,
getString(R.string.rooted_device_message),
Toast.LENGTH_SHORT
).show()
activity.finishAffinity()
}
}
override fun onActivityStarted(p0: Activity) {
appViewModel?.onResume()
}
override fun onActivityResumed(p0: Activity) {}
override fun onActivityPaused(p0: Activity) {}
override fun onActivityStopped(activity: Activity) {
appViewModel?.onPause()
}
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {}
override fun onActivityDestroyed(p0: Activity) {}
}
i keep getting RuntimeException: Cannot create an instance of class x.x.AppViewModel
2022-03-08 22:29:44.189 10889-10889/com.x.x E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.x.x, PID: 10889
java.lang.RuntimeException: Unable to create application com.x.x.App: java.lang.RuntimeException: Cannot create an instance of class com.x.x.AppViewModel
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6991)
at android.app.ActivityThread.access$1700(ActivityThread.java:274)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2093)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:233)
at android.app.ActivityThread.main(ActivityThread.java:8010)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:631)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:978)
Caused by: java.lang.RuntimeException: Cannot create an instance of class com.x.x.AppViewModel
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:230)
at com.x.x.App.onCreate(App.kt:54)
at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1208)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6986)
at android.app.ActivityThread.access$1700(ActivityThread.java:274)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2093)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:233)
at android.app.ActivityThread.main(ActivityThread.java:8010)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:631)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:978)
Caused by: java.lang.NoSuchMethodException: com.x.x.AppViewModel.<init> [class android.app.Application]
at java.lang.Class.getConstructor0(Class.java:2332)
at java.lang.Class.getConstructor(Class.java:1728)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:228)
at com.x.x.App.onCreate(App.kt:54)
at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1208)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6986)
at android.app.ActivityThread.access$1700(ActivityThread.java:274)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2093)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:233)
at android.app.ActivityThread.main(ActivityThread.java:8010)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:631)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:978)
What i am doing wrong in this implementation ?
Is this the right way to implement a viewModel scoped to application ?
From what I can tell, something is trying to call a no argument constructor on your ViewModel, but you have only defined a constructor that takes arguments.
You have arguments in viewmodel constructor. You will have to extend viewmodel factory and handle the parameters passed. Later pass the required parameter values to custom viewmodel factory instance. Finally use the custom view model factory instance in ViewModelProvider.AndroidViewModelFactory(this ,customInstance).create (AppViewModel::class.java)
I have a small app I am using to try learn more about some of the newer Android components. I'm finding it difficult to find information and understand how best to do what I want.
Currently: Open app -> load data + stores in DB -> display data in list
I want to be able to query data again upon button press.
I have 2 buttons, 1 to fetch data again, 1 to delete the list data from the DB.
Problem is that it seems you cannot refresh if you are observing on an instance of LiveData, which I am. I understand that however the way I found to actually do a Network call and store in the Database returns an instance of LiveData and I am not sure how best to proceed.
Let me show you the code.
Fragment
private val viewModel: quoteViewModel by viewModels()
private lateinit var binding: FragmentHomeBinding
private lateinit var adapter: QuoteAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initRecyclerView()
setupRetrieveQuotesObserver()
setupDeleteDataListener()
setupFetchNewDataListener()
setupSwipeToRefresh()
}
private fun initRecyclerView() {
adapter = QuoteAdapter()
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
}
private fun setupDeleteDataListener() {
binding.removeQuotesButton.setOnClickListener {
viewModel.removeAllQuotes()
}
}
private fun setupFetchNewDataListener() {
binding.getQuotesButton.setOnClickListener {
viewModel.removeQuotes()
viewModel.getQuotes()
}
}
private fun setupRetrieveQuotesObserver() {
viewModel.quoteLiveDataList.observe(viewLifecycleOwner, Observer { result ->
when (result.status) {
NewResult.Status.SUCCESS -> {
result.data.let { adapter.setItems(ArrayList(result.data)) }
binding.progressBar.visibility = View.GONE
binding.swipeContainer.isRefreshing = false
}
NewResult.Status.ERROR -> {
binding.progressBar.visibility = View.GONE
Snackbar.make(binding.root, "Some error has occurred", Snackbar.LENGTH_SHORT)
.show()
}
NewResult.Status.LOADING -> {
binding.progressBar.visibility = View.VISIBLE
}
}
})
}
private fun setupSwipeToRefresh() {
binding.swipeContainer.setOnRefreshListener {
viewModel.getQuotes()
}
}
ViewModel
val quoteLiveDataList: LiveData<NewResult<List<Quote>>> = repository.quotes
fun getQuotes() = viewModelScope.launch {
repository.quotes
}
fun removeAllQuotes() = viewModelScope.launch {
repository.deleteAllQuotes()
}
Repository
val quotes = performGetOperation(
databaseQuery = { dao.getAllQuotes() },
networkCall = { remoteSource.getAllQuotes() },
saveCallResult = {
val quotesList = ArrayList<Quote>()
for (messageString in it.messages.non_personalized) {
quotesList.add(
Quote(
messageString,
FaceImageProvider().getRandomFacePicture(),
false
)
)
}
dao.insertQuotes(quotesList)
}
)
#WorkerThread
suspend fun deleteAllQuotes() = withContext(Dispatchers.IO) { dao.deleteAllQuotes() }
performGetOperation
This is a class I saw online for handling what I want to do. I think the issue stems from here as it is returning LiveData, I'm not sure how best to fix it
fun <T, A> performGetOperation(
databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> NewResult<A>,
saveCallResult: suspend (A) -> Unit
): LiveData<NewResult<T>> =
liveData(Dispatchers.IO) {
emit(NewResult.loading())
val source = databaseQuery.invoke().map { NewResult.success(it) }
emitSource(source)
val responseStatus = networkCall.invoke()
if (responseStatus.status == NewResult.Status.SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == NewResult.Status.ERROR) {
emit(NewResult.error(responseStatus.message!!))
emitSource(source)
}
}
RemoteDataSource
suspend fun getQuotes() = getResult { service.getQuotes() }
getResult
protected suspend fun <T> getResult(call: suspend () -> Response<T>): NewResult<T> {
try {
val response = call.invoke()
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
return NewResult.success(body)
}
}
return error("${response.code()} ${response.message()}")
} catch (e: Exception) {
return error(e.message ?: e.toString())
}
}
private fun <T> error(message: String): NewResult<T> {
Log.d("BaseDataSource", message)
return NewResult.error("Network called failed due to: $message")
}
NewResult
data class NewResult<out T>(val status: Status, val data: T?, val message: String?) {
enum class Status {
SUCCESS,
ERROR,
LOADING,
}
companion object {
fun <T> success(data: T): NewResult<T> {
return NewResult(Status.SUCCESS, data, null)
}
fun <T> error(message: String, data: T? = null): NewResult<T> {
return NewResult(Status.ERROR, data, message)
}
fun <T> loading(data: T? = null): NewResult<T> {
return NewResult(Status.LOADING, data, null)
}
}
Apologies for the very long message, but I guess I need to show all the little bits and bobs I'm using.
I think the problem is in the Fragment where I do viewModel.quoteLiveDataList.observe, as it is returning a new LiveData if it is called again. So I'm not sure how I can do another server call and update the DB and return it here.
Appreciate any help!
Thanks
Use Transformations.switchMap on a MutableLiveData to trigger your repository call like it is done here in the GithubBrowserSample project. This will allow you to implement the refresh functionality -
private val _getQuotes = MutableLiveData<Boolean>()
val quotes: LiveData<NewResult<List<Quote>>> = _getQuotes.switchMap { getQuotes ->
repository.quotes
}
fun getQuotes() {
_getQuotes.value = true
}
fun refresh() {
_getQuotes.value?.let {
_getQuotes.value = it
}
}
I have one activity with unspecified orientation and there is one fragment attached to that activity that has different layouts for portrait and landscape mode and on that fragment, multiple API calls on a conditional basis, my problem is that when the screen rotates all data was lost and there is a lot of data on that fragment by which I don't want to save each data on saveInstance method. I tried android:configChanges="keyboardHidden|orientation|screenSize", but this didn't solve my problem. I want to handle this problem using viewModel. Please help, Thanks in advance.
Here is my code
Repository
class GetDataRepository {
val TAG = GetDataRepository::class.java.canonicalName
var job: CompletableJob = Job()
fun getData(
token: String?,
sslContext: SSLContext,
matchId: Int
): LiveData<ResponseModel> {
job = Job()
return object : LiveData<ResponseModel>() {
override fun onActive() {
super.onActive()
job.let { thejob ->
CoroutineScope(thejob).launch {
try {
val apiResponse = ApiService(sslContext).getData(
token
)
LogUtil.debugLog(TAG, "apiResponse ${apiResponse}")
withContext(Dispatchers.Main) {
value = apiResponse
}
} catch (e: Throwable) {
LogUtil.errorLog(TAG, "error: ${e.message}")
withContext(Dispatchers.Main) {
when (e) {
is HttpException -> {
value =
Gson().fromJson<ResponseModel>(
(e as HttpException).response()?.errorBody()
?.string(),
ResponseModel::class.java
)
}
else -> value = ResponseModel(error = e)
}
}
} finally {
thejob.complete()
}
}
}
}
}
}
fun cancelJob() {
job.cancel()
}
}
ViewMode:
class DataViewModel : ViewModel() {
val TAG = DataViewModel::class.java.canonicalName
var mListener: DataListener? = null
private val mGetDataRepository: GetDataRepository = GetDataRepository()
fun getData() {
LogUtil.debugLog(TAG, "getData")
if (mListener?.isInternetAvailable()!!) {
mListener?.onStartAPI()
val context = mListener?.getContext()
val token: String? = String.format(
context?.resources!!.getString(R.string.user_token),
PreferenceUtil.getUserData(context).token
)
val sslContext = mListener?.getSSlContext()
if (sslContext != null) {
val getData =
mGetDataRepository.getData(
token
)
LogUtil.debugLog(TAG, "getData ${getData}")
mListener?.onApiCall(getData)
} else {
LogUtil.debugLog(TAG, "getData Invalid certificate")
mListener?.onError("Invalid certificate")
}
} else {
LogUtil.debugLog(TAG, "getData No internet")
mListener?.onError("Please check your internet connectivity!!!")
}
LogUtil.debugLog(TAG, "Exit getData()")
}
}
Activity:
class DataActivity : AppCompatActivity() {
val TAG = DataActivity::class.java.canonicalName
lateinit var fragment: DataFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LogUtil.debugLog(TAG, "onCreate: Enter")
var binding: ActivityDataBinding =
DataBindingUtil.setContentView(this, R.layout.activity_data)
if (savedInstanceState == null) {
fragment = DataFragment.newInstance()
supportFragmentManager.beginTransaction().add(R.id.container, fragment, DataFragment.TAG)
} else {
fragment = supportFragmentManager.findFragmentByTag(DataFragment.TAG) as DataFragment
}
LogUtil.debugLog(TAG, "onCreate: Exit")
}
}
Fragment:
class DataFragment : Fragment(), DataListener {
private var mBinding: FragmentDataBinding? = null
private lateinit var mViewModel: DataViewModel
companion object {
val TAG = DataFragment::class.java.canonicalName
fun newInstance(): DataFragment {
return DataFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mBinding =
DataBindingUtil.inflate(inflater, R.layout.fragment_data, container, false)
mViewModel = ViewModelProvider(this).get(DataViewModel::class.java)
mViewModel.mListener = this
getData()
return mBinding?.root
}
private fun getData() {
LogUtil.debugLog(TAG, "Enter getMatchScore()")
mViewModel.getData()
LogUtil.debugLog(TAG, "Exit getMatchScore()")
}
override fun <T> onApiCall(response: LiveData<T>) {
response.observe(this, Observer {
it as DataResponseModel
//
})
}
}
The lifecycle of viewModel by default is longer than your activity (in your case, screen rotation).
ViewModel will not be destroyed as soon as activity destroyed for configuration change, you can see this link.
You seem to have made a mistake elsewhere in your activity/fragment, please put your activity/fragment code here.
In your fragment you call mViewModel.getData() in your onCreateView, and every time you rotate your activity, this method call and all store data reset and fetched again!, simply you can check data of ViewModel in your fragment and if it's empty call getData(), it also seems your ViewModel reference to your view(Fragment) (you pass a listener from your fragment to your ViewModel) and it is also an anti-pattern (This article is recommended)