Kotlin: Is it possible to make a function, which calls a retrofit service, to return a String value? - kotlin

I have a Fragment and a View Model.
The layout of the Fragment contains a button.
When the button is clicked, we try to get an API response, which contains a url.
That url is used to start an intent to open a web page.
I am currently accomplishing this with event driven programming.
The button in the Fragment is clicked.
The function in the view model is called to get the API response, which contains the url.
The url in the view model is assigned as live data, which is observed in the fragment.
The fragment observes the url live data has changed. It attempts to launch the WebView with the new url.
Can the Fragment skip Observing for the url and directly get the ViewModel function to return a string?
Here is the code for the Fragment:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Set the OnClickListener
myButton.setOnClickListener {
myViewModel.getUrlQueryResults()
}
// Observables to open WebView from Url
myViewModel.myUrl.observe(viewLifecycleOwner, Observer {
it?.let{
if (it.isEmpty()) {
// No Url found in this API response
}
else {
// Open the WebView
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(it))
startActivity(intent)
}
catch (e: Exception) {
// Log the catch statement
}
}
}
})
}
Here is the code for the ViewModel:
// Live data observed in fragment. When this changes, fragment will attempt to launch Website with the url
private val _myUrl = MutableLiveData<String>()
val myUrl: LiveData<String>
get() = _myUrl
// Possible to make this return a string?
fun getUrlQueryResults() {
InfoQueryApi.retrofitService.getInfo(apiKey).enqueue(object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
try {
// Store the response here
apiResponse = parseInfoJsonResult(JSONObject(response.body()!!))
// Grab the url from the response
var urlFromResponse = apiResponse?.url
if (urlFromResponse.isNullOrEmpty()) {
urlFromResponse = ""
}
// Store the urlFromResponse in the live data so Fragment can Observe and act when the value changes
_myUrl.value = urlFromResponse
} catch (e: Exception) {
// Log catch statement
}
}
override fun onFailure(call: Call<String>, t: Throwable) {
// Log error
}
})
}

Related

What is the best way to get data from an API using retrofit when something needs to wait on that data?

I have a retrofit API call but am having trouble getting the data out of it:
This is in a class file that's not a viewModel or Fragment. It's called from the apps main activity view model. I need to be able to get the data from the API and wait for some processing to be done on it before returning the value back the view model. Newer to kotlin and struggling with all the watchers and async functions. The result of this an empty string is the app crashes, because it's trying to access data before it has a value.
From class getData which is not a fragment
private lateinit var data: Data
fun sync(refresh: Boolean = false): List<String> {
var info = emptyList<String>
try {
getData(::processData, ::onFailure)
info = data.info
} catch(e: Throwable){
throw Exception("failed to get data")
}
}
}
return info
}
fun getData(
onSuccess: KFunction1<ApiResponse>?, Unit>,
onFailed: KFunction1<Throwable, Unit>
) {
val client = ApiClient().create(Service.RequestData)
val request = client.getData()
request.enqueue(object : Callback<ApiResponse> {
override fun onResponse(
call: Call<ApiResponse>,
response: Response<ApiResponse>
) {
onSuccess(response.body())
}
override fun onFailure(call: Call<RegistryResponse<GlobalLanguagePack>>, t: Throwable) {
onFailed(Exception("failed to get data"))
}
})
}
private fun processData(body: ApiResponse?) {
requireNotNull(body)
data = body.data
}
```
From appViewModel.kt:
```
fun setUpStuff(context: Context, resources: AppResources) = viewModelScope.launch {
val stuff = try {
getData.sync()
} catch (e: Exception) {
return#launch
}
if (stuff.isEmpty()) return#launch
}
```

Document references must have an even number of segments

Error: Document references must have an even number of segments, but Users has 1
I have been looking through different posts on here and on different forums but all have the problem when first loading but my problem is after I logout or reset the password. When I load the contents from firebase I get the information but when I click on the sign out then go to login again it crash's and I get this error. I have logged the users.uid and Document references and does not change after logging out.
My collection path is done with Constants so I don't have a mis type.
I have found that the error is in the Fragment side of my app in the FirestoreClass().loadUserData_fragment(this)
As commenting this line out after the log out will allow the app to run but in the activity the data can still be loaded as the activity load data and the fragment is the same so I don't get why it wouldn't load into the fragment after the sign out but will load first time.
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
FirestoreClass().loadUserData_fragment(this)
}
Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityUpdateProfileBinding.inflate(layoutInflater)
val view : LinearLayout = binding.root
setContentView(view)
setupActionBar()
FirestoreClass().loadUserData(this)
}
GetCurrentUserID
fun getCurrentUserID():String{
// auto login
var currentUser = FirebaseAuth.getInstance().currentUser
var currentUserId = ""
if (currentUser != null){
currentUserId = currentUser.uid
Log.i("uis",currentUser.uid)
}
return currentUserId
}
Activity version
fun loadUserData(activity:Activity){
mFireStore.collection(Constants.USERS)
.document(getCurrentUserID())
.get()
.addOnSuccessListener { document ->
val loggedInUser = document.toObject(User::class.java)!!
Log.i("uis",getCurrentUserID() + Constants.USERS)
when(activity){
is UpdateProfileActivity ->{
activity.setUserDataInUI(loggedInUser)
}
is LoginActivity -> {
// Call a function of base activity for transferring the result to it.
activity.userLoggedInSuccess(loggedInUser)
}
}
}
}
Fragment version
fun loadUserData_fragment(fragment: Fragment){
mFireStore.collection(Constants.USERS)
.document(getCurrentUserID())
.get()
.addOnSuccessListener { document ->
val loggedInUser = document.toObject(User::class.java)!!
Log.i("uis",getCurrentUserID() + Constants.USERS)
when(fragment){
is HomeFragment ->{
fragment.setUserDataInUIFragment(loggedInUser)
}
}
}
}
It seems that your getCurrentUserID() returns no value, which you're not handling in your code. The best option is to only call loadUserData when there is an active user, but alternatively you can also check whether getCurrentUserID() returns a value:
fun loadUserData(activity:Activity){
if (getCurrentUserID() != "") { // 👈
mFireStore.collection(Constants.USERS)
.document(getCurrentUserID())
.get()
.addOnSuccessListener { document ->
...
}
}
}

Navigation controller AlertDialog click listner

I'm using Android's Navigation component and I'm wondering how to setup AlertDialog from a fragment with a click listener.
MyFragment
fun MyFragment : Fragment(), MyAlertDailog.MyAlertDialogListener {
...
override fun onDialogPostiveCLick(dialog: DialogFragment) {
Log.i(TAG, "Listener returns a postive click")
}
fun launchMyAlertDialog() {
// Here I would typically call setTargetFragment() and then show the dialog.
// but findnavcontroller doesn't have setTargetFragment()
findNavController.navigate(MyFragmentDirection.actionMyFragmentToMyAlertDialog())
}
}
MyAlertDialog
class MyAlertDialog : DialogFragment() {
...
internal lateinit var listener: MyAlertDialogListener
interface MyAlertDialogListener{
fun onDialogPostiveCLick(dialog: DialogFragment)
}
override fun onCreateDialog(savdInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage("My Dialog message")
.setPositiveButton("Positive", DialogInterface.OnClickListener {
listener = targetFragment as MyAlertDialogListener
listener.onDialogPositiveClick(this)
}
...
}
}
}
This currently receives a null point exception when initializing the listener in MyAlertDialog.
To use targetFragment, you have to set it first as you commented, unfortunately jetpack navigation does not do this for you (hence the null pointer exception). Check out this thread for alternative solution: https://stackoverflow.com/a/50752558/12321475
What I can offer you is an alternative. If the use-case is as simple as displaying a dialog above current fragment, then do:
import androidx.appcompat.app.AlertDialog
...
class MyFragment : Fragment() {
...
fun onDialogPostiveCLick() {
Log.i(TAG, "Listener returns a postive click")
}
fun launchMyAlertDialog() {
AlertDialog.Builder(activity)
.setMessage("My Dialog message")
.setPositiveButton("Positive") { _, _ -> onDialogPostiveCLick() }
.setCancellable(false)
.create().show()
}
}

How can I ensure Kotlin Coroutines are finishing in the correct order?

I am attempting to build a library that allows an app to download a json file I provide, and then based on its contents, download images from the web. I have implemented it thus far with Kotlin Coroutines along with Ktor, but I have an issue which is evading my grasp of what to do.
This is the data class I am using to define each image:
data class ListImage(val name: String, val url: String)
The user calls an init function which downloads the new json file. Once that file is downloaded, the app needs to download a number of images as defined by the file using getImages. Then a list is populated using the data class and an adapter.
Here is the code I am using to fetch the file:
fun init(context: Context, url: String): Boolean {
return runBlocking {
return#runBlocking fetchJsonData(context, url)
}
}
private suspend fun fetchJsonData(context: Context, url: String): Boolean {
return runBlocking {
val client: HttpClient(OkHttp) {
install(JsonFeature) {}
}
val data = async {
client.get<String>(url)
}
try {
val json = data.await()
withContext(Dispatchers.IO) {
context
.openFileOutput("imageFile.json", Context.MODE_PRIVATE)
.use { it.write(json.toByteArray()) }
}
} catch (e: Exception) {
return#runBlocking false
}
}
}
This works and gets the file written locally. Then I have to get the images based on the contents of the file.
suspend fun getImages(context: Context) {
val client = HttpClient(OkHttp)
// Gets the image list from the json file
val imageList = getImageList(context)
for (image in imageList) {
val imageName = image.name
val imageUrl = image.url
runBlocking {
client.downloadFile(context, imageName, imageUrl)
.collect { download ->
if (download == Downloader.Success) {
Log.e("SDK Image Downloader", "Successfully downloaded $imageName.")
} else {
Log.i("SDK Image Downloader", "Failed to download $imageName.")
}
}
}
}
}
private suspend fun HttpClient
.downloadFile(context: Context, fileName: String, url: String): Flow<Downloader> {
return flow {
val response = this#downloadFile.request<HttpResponse> {
url(url)
method = HttpMethod.Get
}
val data = ByteArray(response.contentLength()!!.toInt())
var offset = 0
do {
val currentRead = response.content.readAvailable(data, offset, data.size)
offset += currentRead
} while (currentRead > 0)
if (response.status.isSuccess()) {
withContext(Dispatchers.IO) {
val dataPath =
"${context.filesDir.absolutePath}${File.separator}${fileName}"
File(dataPath).writeBytes(data)
}
emit(Downloader.Success)
} else {
emit(Downloader.Error("Error downloading image $fileName"))
}
}
}
If the file is already on the device and I am not attempting to redownload it, this also works. The issue is when I try to get the file and then the images in order when the app is first run. Here is an example of how I am trying to call it:
lateinit var loaded: Deferred<Boolean>
lateinit var imagesLoaded: Deferred<Unit>
#InternalCoroutinesApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
val context: Context = this
loaded = GlobalScope.async(Dispatchers.Default) {
init(context)
}
GlobalScope.launch { loaded.await() }
imagesLoaded = GlobalScope.async(Dispatchers.Default) {
getDeviceImages(context)
}
GlobalScope.launch { imagesLoaded.await() }
configureImageList(getImageList(context))
}
fun configureImageList(imageList: MutableList<Image>) {
val imageListAdapter = ImageListAdapter(this, imageList)
with(image_list) {
layoutManager = LinearLayoutManager(context)
setHasFixedSize(true)
itemAnimator = null
adapter = imageListAdapter
}
}
This falls apart. So the two scenarios that play out are:
I run this code as-is: The file is downloaded, and ~75% of the images are downloaded before the app crashes with a java.io.IOException: unexpected end of stream on the url. So it seems that the images are starting to download before the file is fully written.
I run the app once without the image code. The file is downloaded. I comment out the file downloading code, and uncomment out the image downloading code. The images are downloaded, the app works as I want. This suggests to me that it would work if the first coroutine was actually finished before the second one started.
I have written and rewritten this code as many ways as I could think of, but I cannot get it to run without incident with both the file writing and image downloading completing successfully.
What am I doing wrong in trying to get these coroutines to complete consecutively?
I just figured it out after stumbling upon this question. It appears as though my HttpClient objects were sharing a connection instead of creating new ones, and when the server closed the connection it caused in-flight operations to unexpectedly end. So the solution is:
val client = HttpClient(OkHttp) {
defaultRequest {
header("Connection", "close")
}
}
I added this to each of the Ktor client calls and it now works as intended.

Avoid fragment recreation when opening from notification navigation component

I want when I click on a notification to open a fragment and not recreate it. I am using navigation component and using NavDeepLinkBuilder
val pendingIntent = NavDeepLinkBuilder(this)
.setComponentName(MainActivity::class.java)
.setGraph(R.navigation.workouts_graph)
.setDestination(R.id.workoutFragment)
.createPendingIntent()
My case is I have a fragment and when you exit the app, there is a notification which when you click on it, it should return you to that same fragment. Problem is every time i click on it it's creating this fragment again, I don't want to be recreated.
I had the same issue. Looks like there is not an option to use the NavDeepLinkBuilder without clearing the stack according to the documentation
I'm not sure the exact nature of your action, but I'll make two assumptions:
You pass the destination id to your MainActivity to navigate.
Your MainActivity is using ViewBinding and has a NavHostFragment
You will have to create the pending intent like:
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
putExtra("destination", R.id.workoutFragment)
}
val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
And in your MainActivity, you can handle both cases (app was already open, app was not already open)
override fun onStart() {
super.onStart()
// called when application was not open
intent?.let { processIntent(it) }
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
// called when application was open
intent?.let { processIntent(it) }
}
private fun processIntent(intent: Intent) {
intent.extras?.getInt("destination")?.let {
intent.removeExtra("destination")
binding.navHostFragment.findNavController().navigate(it)
}
}