I am trying to stop my app from crashing when trying to read json information using Gson and Retrofit2 when user has no internet connection. Any Ideas?
My code:
Fragment
class ManuFragment : Fragment() {
private lateinit var viewModel: ManuFragmentVM
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentManuBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(ManuFragmentVM::class.java)
viewModel.apply {
manufacturer.observe(requireActivity(), {loadRecyclerView(it)})
setup()
}
}
View Model
class ManuFragmentVM : ViewModel() {
val manufacturer = MutableLiveData<List<Manufacturers>>()
fun setup() {
viewModelScope.launch(Dispatchers.Default) {
manufacturer.postValue(CarsRepository().getAllManufacturers())
}
}
Interface
interface ManufacturerApi {
#GET("url.json")
suspend fun fetchAllManufacturers(): List<Manufacturers>
}
Repository
class CarsRepository {
private fun manufacturerRetrofit(): ManufacturerApi {
return Retrofit.Builder()
.baseUrl("https://website.com/")
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
.build()
.create(ManufacturerApi::class.java)
}
suspend fun getAllManufacturers(): List<Manufacturers> {
return manufacturerRetrofit().fetchAllManufacturers()
}
It looks like you only need to wrap your call in a try-catch:
fun setup() {
viewModelScope.launch(Dispatchers.IO) {
try {
manufacturer.postValue(CarsRepository().getAllManufacturers())
} catch (ce: CancellationException) {
throw ce // Needed for coroutine scope cancellation
} catch (e: Exception) {
// display error
}
}
}
Also, you should run API calls explictly with the IO dispatcher. The rest of your code looks really good.
You can wrap manufacturer.postValue(CarsRepository().getAllManufacturers()) with try catch block. If you want to tell user about that error, create another LiveData object, e.g. val error: MutableLiveData<String> and post error text from catch block.
Related
I am using Room and I need to return id to Fragment which is returned when insert().
However, But I couldn't return the value from viewModelScope.
I saw other similar questions, but the answer was to return LiveData.
But I don't need LiveData. I just want to return values of type Long.
How can I do it?
Repo
class WorkoutListRepository(private val dao: WorkoutDao) {
#RequiresApi(Build.VERSION_CODES.O)
suspend fun createDailyLog(part: BodyPart) : Long {
...
return dao.insertDailyLog(data)
}
}
ViewModel
class WorkoutListViewModel(
private val repository: WorkoutListRepository
) : ViewModel() {
...
#RequiresApi(Build.VERSION_CODES.O)
fun createDailyLog(part: BodyPart) : Long {
viewModelScope.launch(Dispatchers.IO) {
return#launch repository.createDailyLog(part) // can't return
}
}
}
Fragment
class WorkoutListTabPagerFragment : Fragment(), WorkoutListAdapter.OnItemClickListener {
...
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentWorkoutListTabPagerBinding.inflate(inflater, container, false)
...
return binding.root
}
#RequiresApi(Build.VERSION_CODES.O)
override fun onItemClick(workout: String) {
when(PageState.curPageState) {
is PageState.startWorkout -> {
val id = vm.createDailyLog(part)
...
}
is PageState.addWorkout -> //TODO:
is PageState.editWorkout -> //TODO:
}
}
}
But I don't need LiveData
You do. You need some kind of observable data holder because the code inside launch is asynchronous. It doesn't run immediately. It is only kind of scheduled for execution. launch function, on the other hand, returns immediately, i.e. your createDailyLog function in ViewModel returns before the call to repository.createDailyLog(part) is made. So you can't return a value synchronously from an asynchronous method.
You could either use LiveData or Kotlin's StateFlow to send this data to the Fragment. Your fragment will observe changes to that state and respond accordingly. I suggest using StateFlow here. The code will look somewhat like this:
// ViewModel
class WorkoutListViewModel(
private val repository: WorkoutListRepository
) : ViewModel() {
private val _logIdFlow = MutableStateFlow<Long?>(null)
val logIdFlow = _logIdFlow.asStateFlow()
...
#RequiresApi(Build.VERSION_CODES.O)
fun createDailyLog(part: BodyPart) : Long {
viewModelScope.launch(Dispatchers.IO) {
_logIdFlow.value = repository.createDailyLog(part)
}
}
}
// Fragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentWorkoutListTabPagerBinding.inflate(inflater, container, false)
...
viewLifecycleOwner.lifecycleScope.launch {
viewModel.logIdFlow.collect { logId ->
if(logId != null) {
// Do whatever you want with the log Id
}
}
}
return binding.root
}
An alternate solution can be to use Kotlin Channel and send data through that Channel.
If you just need a quick, short solution, you can call the repository function from the Fragment's lifecycle scope directly, like this:
// ViewModel
suspend fun createDailyLog(part: BodyPart) : Long {
return repository.createDailyLog(part)
}
//Fragment
override fun onItemClick(workout: String) {
viewLifecycleOwner.lifecycleScope.launch {
when(PageState.curPageState) {
is PageState.startWorkout -> {
val id = vm.createDailyLog(part) // This will now work
...
}
is PageState.addWorkout -> //TODO:
is PageState.editWorkout -> //TODO:
}
}
}
The only problem with this solution is that, now db operation is tied to fragment's lifecycle. So if there is any event which destroy's fragment's lifecycle (like a config change), the operation will be cancelled. This shouldn't be that big of an issue here as your db operation will only take a few milliseconds. But the first option of using a StateFlow or Channel to send data to Fragment/Activity is a more general and recommended way. You can go with whichever option you like.
I recently started studying android kotlin and I am expecting this to have a very simple answer but I have a ViewModel which has a method called getDataComment and I want to call the method from my fragment and provide the needed argument for it. But when I try to call it, there's no build error, it just doesn't show the list.
My method in view model:
fun getDataComment(postId: Int) {
PostRepository().getDataComment(postId).enqueue(object : Callback<List<Comment>>{
override fun onResponse(call: Call<List<Comment>>, response: Response<List<Comment>>) {
val comments = response.body()
comments?.let {
mPostsComment.value = comments!!
}
}
override fun onFailure(call: Call<List<Comment>>, t: Throwable) {
Log.e(TAG, "On Failure: ${t.message}")
t.printStackTrace()
}
})
}
Fragment Class
class PostDetailFragment : Fragment() {
var param2: Int = 0
private lateinit var binding: FragmentPostDetailBinding
private val viewModel by viewModels<PostCommentViewModel>()
private val gson: Gson by lazy {
Gson()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding =
DataBindingUtil.inflate(inflater, R.layout.fragment_post_detail, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
checkArguments()
}
private fun checkArguments() {
arguments?.let { bundle ->
if (bundle.containsKey("POST")) {
val postString = bundle.getString("POST", "")
val post: Post = gson.fromJson(postString, Post::class.java)
binding.postDetailstvTitle.text = post.title
binding.postDetailstvBody.text = post.body
param2 = post.id
viewModel.getDataComment(param2)
}
}
}
}
Here's what it's supposed to look like: 1
Here's what I'm getting: 2
Sorry in advance if It's messy this is my first time asking here and let me know if you need more information. Thank you!
observe mPostsComment inside fragment from viewmodel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
checkArguments()
viewModel.mPostsComment.observe(viewLifecycleOwner, { collectionList ->
// do whatever you want with collectionList
})
}
I just started learning MVVM and I am unable to find out why I am getting response from retrofit multiple times.
I am following the videos on how to implement MVVM in kotlin and I would be very glad if you suggested better approach to this.
This is the Repository (I am using jsonplaceholder to get users):
class HelpersRepository(val application: Application) {
val showProgress = MutableLiveData<Boolean>()
val helpersList = MutableLiveData<List<Helpers>>()
fun getAllHelpers() {
showProgress.value = true
val destinationService = ServiceBuilder.buildService(HelpersNetwork::class.java)
val requestCall = destinationService.getHelpers()
requestCall.enqueue(object : Callback<List<Helpers>>{
override fun onResponse(call: Call<List<Helpers>>, response: Response<List<Helpers>>) {
helpersList.value = response.body()
Log.d("response", "${helpersList.value}")
showProgress.value = false
}
override fun onFailure(call: Call<List<Helpers>>, t: Throwable) {
showProgress.value = false
Toast.makeText(application, "Error while fetching the data", Toast.LENGTH_SHORT).show()
}
})
}}
This is the ViewModel:
class HelpersViewModel(application: Application) : AndroidViewModel(application) {
private val repository = HelpersRepository(application)
val showProgress : LiveData<Boolean>
val helpersList : LiveData<List<Helpers>>
init {
this.showProgress = repository.showProgress
this.helpersList = repository.helpersList
}
fun getAllHelpers() {
repository.getAllHelpers()
}
}
And this is my Fragment:
class HelpersFragment : Fragment() {
private lateinit var viewModel: HelpersViewModel
private lateinit var adapter: HelpersAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_helpers, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(HelpersViewModel::class.java)
adapter = HelpersAdapter(requireContext())
rv_helpers.adapter = adapter
viewModel.getAllHelpers()
viewModel.showProgress.observe(viewLifecycleOwner, Observer {
if (it) {
pb_helpers.visibility = View.VISIBLE
} else {
pb_helpers.visibility = View.GONE
}
})
viewModel.helpersList.observe(viewLifecycleOwner, Observer {
adapter.setHelpersList(it)
})
}
}
when viewModel.getAllHelpers() gets called in the fragment, the log displays the response three times (I am unable to copy the log as StackOverflow considers it as a spam).
P.S. If I put viewModel.getAllHelpers() inside a setOnclickListener on button, then it only fetches data one time.
I have searched everywhere but was unable to get the answer fitted to my question. I realize I am doing something wrong, but can't figure out exactly what
I finally figured out that my problem was coming from choosing dark mode in MainActivity. Turns out you must call:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
before setContentView() in onCreate
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)
}
}
I have this Fragment that just serves as a splash screen while data is retrieved. The problem is that on a configuration change or if the Fragment is offscreen (user navigated out of the app) it crashes when it returns from the IO Coroutine block and tries to execute the navigation in the Main Coroutine block.
Here is the code:
Note: viewModel.repository.initData() makes a Retrofit call and persists the response to a Room database if data doesn't exist or is stale.
class LoadingFragment : Fragment() {
private lateinit var viewModel: LoadingViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_loading, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(LoadingViewModel::class.java)
CoroutineScope(Dispatchers.IO).launch {
// Small delay so the user can actually see the splash screen
// for a moment as feedback of an attempt to retrieve data.
delay(250)
try {
viewModel.repository.initData()
CoroutineScope(Dispatchers.Main).launch {
findNavController().navigate(R.id.action_loadingFragment_to_mainFragment)
}
} catch (e: IOException) {
findNavController().navigate(R.id.action_loadingFragment_to_errorFragment)
}
}
}
}
Also I need the navigation to take place only after the data is retrieved but the data retrieval has to be done on the IO thread and the navigation on the main thread.
I have been reading about scoping the Coroutine but I am still confused/unsure how it works and how to properly set it up.
I was able to fix it by implementing something like this:
class LoadingFragment : Fragment() {
private lateinit var viewModel: LoadingViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_loading, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(LoadingViewModel::class.java)
lifecycleScope.launch {
withContext(Dispatchers.IO) {
// Small delay so the user can actually see the splash screen
// for a moment as feedback of an attempt to retrieve data.
delay(250)
try {
viewModel.initData()
withContext(Dispatchers.Main) {
findNavController().navigate(R.id.action_loadingFragment_to_mainFragment)
}
} catch (e: IOException) {
findNavController().navigate(R.id.action_loadingFragment_to_errorFragment)
}
}
}
}
}