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)
Related
in a class which extends BroadcasrReceiver, I want to access a repository function.
I get an error lateinit property chargeSessionRepo has not been initialized.
I have specified #Inject, what else needs to be done, so I can access the function in ChargeSessionRepository
class CustomNotificationListener : BroadcastReceiver() {
#Inject
lateinit var chargeSessionRepo: ChargeSessionRepository
override fun onReceive(context: Context, intent: Intent) {
Log.d("fcmService", "onReceive ${intent.getStringExtra("onClickListener")}")
when(intent.getStringExtra("onClickListener")) {
"approve" -> {
approvePlugin(true)
}
"deny" -> {
approvePlugin(false)
}
}
}
private fun approvePlugin(isApproved: Boolean) {
GlobalScope.launch {
try {
val chargeStatusUpdate = chargeSessionRepo.getChargeStatusUpdate().data
val chargeStatus = chargeStatusUpdate?.firstOrNull()
chargeStatus?.sessionId?.let {
chargeSessionRepo.approvePlugIn(it, isApproved)
}
} catch (exception: Exception) {
Log.d("fcmService", "exception: ${exception}") // lateinit property chargeSessionRepo has not been initialized
}
}
}
}
ChargesessionRepository
#Singleton
class ChargeSessionRepository #Inject constructor(
private val chargeSessionService: ChargeSessionService,
private val chargeSessionDao: ChargeSessionDao
) : Repository() {
var lastChargeStatus: List<ChargeStatusDTO>? = null
private set
suspend fun getChargeStatusUpdate(): Resource<List<ChargeStatusDTO>> {
val resource = getResource { chargeSessionService.getChargeStatus() }
lastChargeStatus = resource.data
return resource
}
}
could you please suggest how to do this
Thanks
R
You may have forgotten to put #AndroidEntryPointon top of your broadcast receiver. if you are using hilt.
I have been trying, without success, to do some UI tests on Android.
My app follows the MVVM architecture and uses Koin for DI.
I followed this tutorial to properly set up a UI test for a Fragment with Koin, MockK and Kakao.
I created the custom rule for injecting mocks, setup the ViewModel, and on the #Before call, run the expected answers and returns with MockK. The problem is that, even when the fragment's viewmodel's LiveData object is the same as the testing class's LiveData object, the Observer's onChange is never triggered on the Fragment.
I run the test with the debugger and it seems the LiveData functions and MockK's answers are properly called. The logs show that the value hold by the LiveData objects is the same. The lifecycle of the Fragment when the test is running is Lifecycle.RESUMED. So why is the Observer's onChange(T) not being triggered?
The custom rule:
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
abstract class FragmentTestRule<F : Fragment> :
ActivityTestRule<FragmentActivity>(FragmentActivity::class.java, true, true) {
override fun afterActivityLaunched() {
super.afterActivityLaunched()
activity.runOnUiThread {
val fm = activity.supportFragmentManager
val transaction = fm.beginTransaction()
transaction.replace(
android.R.id.content,
createFragment()
).commit()
}
}
override fun beforeActivityLaunched() {
super.beforeActivityLaunched()
val app = InstrumentationRegistry.getInstrumentation()
.targetContext.applicationContext as VideoWorldTestApp
app.injectModules(getModules())
}
protected abstract fun createFragment(): F
protected abstract fun getModules(): List<Module>
fun launch() {
launchActivity(Intent())
}
}
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <F : Fragment> createRule(fragment: F, vararg module: Module): FragmentTestRule<F> =
object : FragmentTestRule<F>() {
override fun createFragment(): F = fragment
override fun getModules(): List<Module> = module.toList()
}
My test App:
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
class VideoWorldTestApp: Application(){
companion object {
lateinit var instance: VideoWorldTestApp
}
override fun onCreate() {
super.onCreate()
instance = this
startKoin {
if (BuildConfig.DEBUG) androidLogger(Level.DEBUG) else EmptyLogger()
androidContext(this#VideoWorldTestApp)
modules(emptyList())
}
Timber.plant(Timber.DebugTree())
}
internal fun injectModules(modules: List<Module>) {
loadKoinModules(modules)
}
}
The custom test runner:
class CustomTestRunner: AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, VideoWorldTestApp::class.java.name, context)
}
}
The test:
#RunWith(AndroidJUnit4ClassRunner::class)
class HomeFragmentTest {
private val twitchViewModel: TwitchViewModel = mockk(relaxed = true)
private val userData = MutableLiveData<UserDataResponse>()
private val fragment = HomeFragment()
#get:Rule
var fragmentRule = createRule(fragment, module {
single(override = true) {
twitchViewModel
}
})
#get:Rule
var countingTaskExecutorRule = CountingTaskExecutorRule()
#Before
fun setup() {
val userResponse: UserResponse = mockk()
every { userResponse.displayName } returns "Rubius"
every { userResponse.profileImageUrl } returns ""
every { userResponse.description } returns "Soy streamer"
every { userResponse.viewCount } returns 5000
every { twitchViewModel.userData } returns userData as LiveData<UserDataResponse>
every { twitchViewModel.getUserByInput(any()) }.answers {
userData.value = UserDataResponse(listOf(userResponse))
}
}
#Test //This one is passing
fun testInitialViewState() {
onScreen<HomeScreen> {
streamerNameTv.containsText("")
streamerCardContainer.isVisible()
nameInput.hasEmptyText()
progressBar.isGone()
}
}
#Test //This one is failing
fun whenWritingAName_AndPressingTheImeAction_AssertTextChanges() {
onScreen<HomeScreen> {
nameInput.typeText("Rubius")
//nameInput.pressImeAction()
searchBtn.click()
verify { twitchViewModel.getUserByInput(any()) } //This passes
countingTaskExecutorRule.drainTasks(5, TimeUnit.SECONDS)
streamerNameTv.hasText("Rubius") //Throws exception
streamerDescp.hasText("Soy streamer")
streamerCount.hasText("Views: ${5000.formatInt()}}")
}
}
}
The fragment being tested:
class HomeFragment : BaseFragment<FragmentHomeBinding>(R.layout.fragment_home) {
override val bindingFunction: (view: View) -> FragmentHomeBinding
get() = FragmentHomeBinding::bind
val twitchViewModel: TwitchViewModel by sharedViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
twitchViewModel.getUserClips("")
binding.nameInput.setOnEditorActionListener { _, actionId, _ ->
if(actionId == EditorInfo.IME_ACTION_SEARCH) {
twitchViewModel.getUserByInput(binding.nameInput.text.toString())
hideKeyboard()
return#setOnEditorActionListener true
}
return#setOnEditorActionListener false
}
binding.searchBtn.setOnClickListener {
twitchViewModel.getUserByInput(binding.nameInput.text.toString() ?: "")
hideKeyboard()
}
twitchViewModel.userData.observe(viewLifecycleOwner, Observer { data ->
if (data != null && data.dataList.isNotEmpty()){
binding.streamerCard.setOnClickListener {
findNavController().navigate(R.id.action_homeFragment_to_clipsFragment)
}
val streamer = data.dataList[0]
Picasso.get()
.load(streamer.profileImageUrl)
.into(binding.profileIv)
binding.streamerLoginTv.text = streamer.displayName
binding.streamerDescpTv.text = streamer.description
binding.streamerViewCountTv.text = "Views: ${streamer.viewCount.formatInt()}"
}
else {
binding.streamerCard.setOnClickListener { }
}
})
twitchViewModel.errorMessage.observe(viewLifecycleOwner, Observer { msg ->
showSnackbar(msg)
})
twitchViewModel.progressVisibility.observe(viewLifecycleOwner, Observer { visibility ->
binding.progressBar.visibility = visibility
binding.cardContent.visibility =
if(visibility == View.VISIBLE)
View.GONE
else
View.VISIBLE
})
}
}
The ViewModel:
class TwitchViewModel(private val repository: TwitchRepository): BaseViewModel() {
private val _userData = MutableLiveData<UserDataResponse>()
val userData = _userData as LiveData<UserDataResponse>
private val _userClips = MutableLiveData<UserClipsResponse?>()
val userClips = _userClips as LiveData<UserClipsResponse?>
init {
viewModelScope.launch {
repository.authUser(this#TwitchViewModel)
}
}
fun currentUserId() = userData.value?.dataList?.get(0)?.id ?: ""
fun clipsListExists() = userClips.value != null
fun getUserByInput(input: String){
viewModelScope.launch {
_progressVisibility.value = View.VISIBLE
_userData.value = repository.getUserByName(input, this#TwitchViewModel)
_progressVisibility.value = View.GONE
}
}
/**
* #param userId The ID of the Streamer whose clips are gonna fetch. If null, resets
* If empty, sets the [userClips] value to null.
*/
fun getUserClips(userId: String){
if(userId.isEmpty()) {
_userClips.postValue(null)
return
}
if(userId == currentUserId() && _userClips.value != null) {
_userClips.postValue(_userClips.value)
return
}
viewModelScope.launch {
_userClips.value = repository.getUserClips(userId, this#TwitchViewModel)
}
}
}
When running the test with the normal ActivityRule and launching the Activity as it were a normal launch, the observers are triggering successfully.
I'm using a relaxed mock to avoid having to mock all functions and variables.
Finally found the problem and the solution with the debugger. Apparently, the #Before function call runs after the ViewModel is injected into the fragment, so even if the variables pointed to the same reference, mocked answer where executing only in the test context, not in the android context.
I changed the ViewModel initialization to the module scope like this:
#get:Rule
val fragmentRule = createRule(fragment, module {
single(override = true) {
makeMocks()
val twitchViewModel = mockViewModel()
twitchViewModel
}
})
private fun makeMocks() {
mockkStatic(Picasso::class)
}
private fun mockViewModel(): TwitchViewModel {
val userData = MutableLiveData<UserDataResponse>()
val twitchViewModel = mockk<TwitchViewModel>(relaxed = true)
every { twitchViewModel.userData } returns userData
every { twitchViewModel.getUserByInput("Rubius") }.answers {
updateUserDataLiveData(userData)
}
return twitchViewModel
}
And the Observer inside the Fragment got called!
Maybe it's not related, but I could not rebuild the gradle project if I have mockk(v1.10.0) as a testImplementation and as a debugImplementation.
I have the following ViewModel class -
class VerifyOtpViewModel : ViewModel() {
private var existingUserProfileData: MutableLiveData<TwoVerteUsers.TwoVerteUser>? = null
fun checkInfoForAuthenticatedUser(authorization: String, user: String) {
ProfileNetworking.getUsersProfiles(authorization, GetUserProfilesBodyModel(listOf(user)), object : ProfileNetworking.OnGetUserProfilesListener {
override fun onSuccess(model: TwoVerteUsers) {
existingUserProfileData?.value = model[0]
}
override fun onError(reason: String) {
Log.d("existingProfile", reason)
}
})
}
fun getExistingUserProfileData(): LiveData<TwoVerteUsers.TwoVerteUser>? {
if (existingUserProfileData == null) return null
return existingUserProfileData as LiveData<TwoVerteUsers.TwoVerteUser>
}
}
and the following observer -
private fun initViewModel() {
verifyOtpViewModel = ViewModelProvider(this).get(VerifyOtpViewModel::class.java)
verifyOtpViewModel.getExistingUserProfileData()?.observe(this, Observer {
if (it != null)
Log.d("existingProfile", it.username)
})
}
For some reason the observe is never triggered even after the MutableLiveData object is being given a value
Tried to search for a solution here at stackoverflow but nothing helped
what am I missing?
refactor your code to this, and you should be good to go:
class VerifyOtpViewModel : ViewModel() {
private val _existingUserProfileData = MutableLiveData<TwoVerteUsers.TwoVerteUser>()
val existingUserProfileData: LiveData<TwoVerteUsers.TwoVerteUser>
get() = _existingUserProfileData
fun checkInfoForAuthenticatedUser(authorization: String, user: String) {
ProfileNetworking.getUsersProfiles(
authorization,
GetUserProfilesBodyModel(listOf(user)),
object : ProfileNetworking.OnGetUserProfilesListener {
override fun onSuccess(model: TwoVerteUsers) {
existingUserProfileData.value = model[0]
}
override fun onError(reason: String) {
Log.d("existingProfile", reason)
}
})
}
}
And observing:
verifyOtpViewModel.existingUserProfileData.observe(this, Observer {
.....
})
I am trying to follow Pusher Chatkit's tutorial for "BUILDING A GROUP CHAT APP USING KOTLIN AND PUSHER CHATKIT" however I am running into an issue with the Recycler View adapter. FYI, I am still learning Kotlin. I've been reading up on lateinit vars but I can't find anything that addresses this case. The error occurs in the recycler view adapter.
This is the error I get:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.android_myneighborsbookshelf, PID: 26692
kotlin.UninitializedPropertyAccessException: lateinit property roomClickedInterface has not been initialized
at com.example.android_myneighborsbookshelf.adapters.ChatRoomsListAdapter.getRoomClickedInterface(ChatRoomsListAdapter.kt:13)
at com.example.android_myneighborsbookshelf.adapters.ChatRoomsListAdapter$ViewHolder.onClick(ChatRoomsListAdapter.kt:66)
at android.view.View.performClick(View.java:6597)
at android.view.View.performClickInternal(View.java:6574)
at android.view.View.access$3100(View.java:778)
at android.view.View$PerformClick.run(View.java:25885)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
ChatRoomsListActivity.kt
class ChatRoomsListActivity : AppCompatActivity() {
val adapter = ChatRoomsListAdapter();
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat_room_list)
initRecyclerView()
initChatManager()
}
private fun initRecyclerView() {
recycler_view.layoutManager = LinearLayoutManager(this#ChatRoomsListActivity)
recycler_view.adapter = adapter
}
private fun initChatManager() {
val chatManager = ChatManager(
instanceLocator = "blahblahblah",
userId = "username1-PCKid",
dependencies = AndroidChatkitDependencies(
tokenProvider = ChatkitTokenProvider(
endpoint = "blahblahblah",
// endpoint = "http://10.0.2.2:3000/auth",
userId = "username1-PCKid"
)
)
)
chatManager.connect(listeners = ChatListeners(
onErrorOccurred = { },
onAddedToRoom = { },
onRemovedFromRoom = { },
onCurrentUserReceived = { },
onNewReadCursor = { },
onRoomDeleted = { },
onRoomUpdated = { },
onPresenceChanged = { u, n, p -> },
onUserJoinedRoom = { u, r -> },
onUserLeftRoom = { u, r -> },
onUserStartedTyping = { u, r -> },
onUserStoppedTyping = { u, r -> }
)) { result ->
when (result) {
is Result.Success -> {
// We have connected!
val currentUser = result.value
AppController.currentUser = currentUser
val userJoinedRooms = ArrayList<Room>(currentUser.rooms)
for (i in 0 until userJoinedRooms.size) {
adapter.addRoom(userJoinedRooms[i])
}
currentUser.getJoinableRooms { result ->
when (result) {
is Result.Success -> {
// Do something with List<Room>
val rooms = result.value
runOnUiThread {
for (i in 0 until rooms.size) {
adapter.addRoom(rooms[i])
}
}
}
}
}
adapter.setInterface(object : ChatRoomsListAdapter.RoomClickedInterface {
override fun roomSelected(room: Room) {
if (room.memberUserIds.contains(currentUser.id)) {
// user already belongs to this room
roomJoined(room)
} else {
currentUser.joinRoom(
roomId = room.id,
callback = { result ->
when (result) {
is Result.Success -> {
// Joined the room!
roomJoined(result.value)
}
is Result.Failure -> {
Log.d("TAG", result.error.toString())
}
}
}
)
}
}
})
}
is Result.Failure -> {
// Failure
Log.d("TAG", result.error.toString())
}
}
}
}
private fun roomJoined(room: Room) {
val intent = Intent(this#ChatRoomsListActivity, ChatRoomsListActivity::class.java)
intent.putExtra("room_id", room.id)
intent.putExtra("room_name", room.name)
startActivity(intent)
}
}
RecyclerView Adapter
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.pusher.chatkit.rooms.Room
import android.view.LayoutInflater
import androidx.recyclerview.widget.RecyclerView
import com.example.android_myneighborsbookshelf.R
class ChatRoomsListAdapter: RecyclerView.Adapter<ChatRoomsListAdapter.ViewHolder>() {
private var list = ArrayList<Room>()
lateinit var roomClickedInterface:RoomClickedInterface // <-- Error occurs here - kt.13
fun addRoom(room:Room){
list.add(room)
notifyDataSetChanged()
}
fun setInterface(roomClickedInterface:RoomClickedInterface){
this.roomClickedInterface = roomClickedInterface
}
override fun getItemCount(): Int {
return list.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(
android.R.layout.simple_list_item_1,
parent,
false
)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.roomName.text = list[position].name
}
inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView), View.OnClickListener {
override fun onClick(p0: View?) {
roomClickedInterface.roomSelected(list[adapterPosition]) // <-- Error occurs here kt.66
}
var roomName: TextView = itemView.findViewById(android.R.id.text1)
init {
itemView.setOnClickListener(this)
}
}
interface RoomClickedInterface{
fun roomSelected(room:Room)
}
}
Any and all help is appreciated.
lateinit means late initialization. If you do not want to initialize
a variable in the constructor, instead you want to initialize it later
on and if you can guarantee the initialization before using it, then
declare that variable with lateinit keyword. It will not allocate
memory until initialized.
So, You have to initialize the lateinit property before try to use it.
Option - 1: Call setInterface() to initialize the property before click the list item. You can also check whether a lateinit var has already been initialized or not using .isInitialized like below:
override fun onClick(p0: View?) {
if(this::roomClickedInterface.isInitialized) {
roomClickedInterface.roomSelected(list[adapterPosition])
}
}
Option - 2: Remove lateinit property from your adapter and also it's setter
//lateinit var roomClickedInterface:RoomClickedInterface
And pass RoomClickedInterface as parameter of your adapter's constructor
class ChatRoomsListAdapter(val roomClickedInterface:RoomClickedInterface):
RecyclerView.Adapter<ChatRoomsListAdapter.ViewHolder>() {
//Implementation
}
Option - 3: Remove lateinit and use nullable property
private var roomClickedInterface: RoomClickedInterface? = null
And then use like below using null safety notation ?.:
override fun onClick(p0: View?) {
roomClickedInterface?.roomSelected(list[adapterPosition])
}
I have an activity to perform rest API everytime it opened and i use MVVM pattern for this project. But with this snippet code i failed to get updated everytime i open activity. So i debug all my parameters in every line, they all fine the suspect problem might when apiService.readNewsAsync(param1,param2) execute, my postValue did not update my resulRead parameter. There were no crash here, but i got result which not updated from result (postValue). Can someone explain to me why this happened?
Here what activity looks like
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DataBindingUtil.setContentView<ActivityReadBinding>(this,
R.layout.activity_read).apply {
this.viewModel = readViewModel
this.lifecycleOwner = this#ReadActivity
}
readViewModel.observerRead.observe(this, Observer {
val sukses = it.isSuccess
when{
sukses -> {
val data = it.data as Read
val article = data.article
//Log.d("-->", "${article.toString()}")
}
else -> {
toast("ada error ${it.msg}")
Timber.d("ERROR : ${it.msg}")
}
}
})
readViewModel.getReadNews()
}
Viewmodel
var observerRead = MutableLiveData<AppResponse>()
init {
observerRead = readRepository.observerReadNews()
}
fun getReadNews() {
// kanal and guid i fetch from intent and these value are valid
loadingVisibility = View.VISIBLE
val ok = readRepository.getReadNews(kanal!!, guid!!)
if(ok){
loadingVisibility = View.GONE
}
}
REPOSITORY
class ReadRepositoryImpl private constructor(private val newsdataDao: NewsdataDao) : ReadRepository{
override fun observerReadNews(): MutableLiveData<AppResponse> {
return newsdataDao.resultRead
}
override fun getReadNews(channel: String, guid: Int) = newsdataDao.readNews(channel, guid)
companion object{
#Volatile private var instance: ReadRepositoryImpl? = null
fun getInstance(newsdataDao: NewsdataDao) = instance ?: synchronized(this){
instance ?: ReadRepositoryImpl(newsdataDao).also {
instance = it
}
}
}
}
MODEL / DATA SOURCE
class NewsdataDao {
private val apiService = ApiClient.getClient().create(ApiService::class.java)
var resultRead = MutableLiveData<AppResponse>()
fun readNews(channel: String, guid: Int): Boolean{
GlobalScope.launch {
val response = apiService.readNewsAsync(Constants.API_TOKEN, channel, guid.toString()).await()
when{
response.isSuccessful -> {
val res = response.body()
val appRes = AppResponse(true, "ok", res!!)
resultRead.postValue(appRes)
}
else -> {
val appRes = AppResponse(false, "Error: ${response.message()}", null)
resultRead.postValue(appRes)
}
}
}
return true
}
}
Perhaps this activity is not getting stopped.
Check this out:
When you call readViewModel.getReadNews() in onCreate() your activity is created once, only if onStop is called will it be created again.