Implementing pagination with Retrofit in Eventyay Attendee

Pagination (Paging) is a common and powerful technique in Android Development when making HTTP requests or fetching data from the database. Eventyay Attendee has found many situations where data binding comes in as a great solution for our network calls with Retrofit. Let’s take a look at this technique.

  • Problems without Pagination in Android Development
  • Implementing Pagination with Kotlin with Retrofit
  • Results and GIF
  • Conclusions

PROBLEMS WITHOUT DATABINDING IN ANDROID DEVELOPMENT

Making HTTP requests to fetch data from the API is a basic work in any kind of application. With the mobile application, network data usage management is an important factor that affects the loading performance of the app. Without paging, all of the data are fetched even though most of them are not displayed on the screen. Pagination is a technique to load all the data in pages of limited items, which is much more efficient

IMPLEMENTING DATABINDING IN FRAGMENT VIEW

Step 1:  Set up dependency in build.gradle

// Paging
implementation "androidx.paging:paging-runtime:$paging_version"
implementation "androidx.paging:paging-rxjava2:$paging_version"

Step 2:  Set up retrofit to fetch events from the API

@GET("events?include=event-sub-topic,event-topic,event-type")
fun searchEventsPaged(
   @Query("sort") sort: String,
   @Query("filter") eventName: String,
   @Query("page[number]") page: Int,
   @Query("page[size]") pageSize: Int = 5
): Single<List<Event>>

Step 3: Set up the DataSource

DataSource is a base class for loading data in the paging library from Android. In Eventyay, we use PageKeyedDataSource. It will fetch the data based on the number of pages and items per page with our default parameters. With PageKeyedDataSource, three main functions loadInitial(), loadBefore(), loadAfter() are used to to load each chunks of data.

class EventsDataSource(
   private val eventService: EventService,
   private val compositeDisposable: CompositeDisposable,
   private val query: String?,
   private val mutableProgress: MutableLiveData<Boolean>

) : PageKeyedDataSource<Int, Event>() {
   override fun loadInitial(
       params: LoadInitialParams<Int>,
       callback: LoadInitialCallback<Int, Event>
   ) {
       createObservable(1, 2, callback, null)
   }

   override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Event>) {
       val page = params.key
       createObservable(page, page + 1, null, callback)
   }

   override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Event>) {
       val page = params.key
       createObservable(page, page - 1, null, callback)
   }

   private fun createObservable(
       requestedPage: Int,
       adjacentPage: Int,
       initialCallback: LoadInitialCallback<Int, Event>?,
       callback: LoadCallback<Int, Event>?
   ) {
       compositeDisposable +=
           eventService.getEventsByLocationPaged(query, requestedPage)
               .withDefaultSchedulers()
               .subscribe({ response ->
                   if (response.isEmpty()) mutableProgress.value = false
                   initialCallback?.onResult(response, null, adjacentPage)
                   callback?.onResult(response, adjacentPage)
               }, { error ->
                   Timber.e(error, "Fail on fetching page of events")
               }
           )
   }
}

Step 4: Set up the Data Source Factory

DataSourceFactory is the class responsible for creating DataSource object so that we can create PagedList (A type of List used for paging) for events.

class EventsDataSourceFactory(
   private val compositeDisposable: CompositeDisposable,
   private val eventService: EventService,
   private val query: String?,
   private val mutableProgress: MutableLiveData<Boolean>
) : DataSource.Factory<Int, Event>() {
   override fun create(): DataSource<Int, Event> {
       return EventsDataSource(eventService, compositeDisposable, query, mutableProgress)
   }
}

Step 5: Adapt the current change to the ViewModel. 

Previously, events fetched in List<Event> Object are now should be turned into PagedList<Event>.

sourceFactory = EventsDataSourceFactory(
   compositeDisposable,
   eventService,
   mutableSavedLocation.value,
   mutableProgress
)
val eventPagedList = RxPagedListBuilder(sourceFactory, config)
   .setFetchScheduler(Schedulers.io())
   .buildObservable()
   .cache()

compositeDisposable += eventPagedList
   .subscribeOn(Schedulers.io())
   .observeOn(AndroidSchedulers.mainThread())
   .distinctUntilChanged()
   .doOnSubscribe {
       mutableProgress.value = true
   }.subscribe({
       val currentPagedEvents = mutablePagedEvents.value
       if (currentPagedEvents == null) {
           mutablePagedEvents.value = it
       } else {
           currentPagedEvents.addAll(it)
           mutablePagedEvents.value = currentPagedEvents
       }
   }, {
       Timber.e(it, "Error fetching events")
       mutableMessage.value = resource.getString(R.string.error_fetching_events_message)
   })

Step 6: Turn ListAdapter into PagedListAdapter

PageListAdapter is basically the same ListAdapter to update the UI of the events item but specifically used for Pagination. In here, List objects can also be null.

class EventsListAdapter : PagedListAdapter<Event, EventViewHolder>(EventsDiffCallback()) {

   var onEventClick: EventClickListener? = null
   var onFavFabClick: FavoriteFabClickListener? = null
   var onHashtagClick: EventHashTagClickListener? = null

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventViewHolder {
       val binding = ItemCardEventsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
       return EventViewHolder(binding)
   }

   override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
       val event = getItem(position)
       if (event != null)
           holder.apply {
               bind(event, position)
               eventClickListener = onEventClick
               favFabClickListener = onFavFabClick
               hashTagClickListAdapter = onHashtagClick
           }
   }

   /**
    * The function to call when the adapter has to be cleared of items
    */
   fun clear() {
       this.submitList(null)
   }

AND HERE ARE THE RESULTS…

CONCLUSION

Databinding is the way to go when working with a complex UI in Android Development. This helps reducing boilerplate code and to increase the readability of the code and the performance of the UI. One problem with data-binding is that sometimes, it is pretty hard to debug with unhelpful log messages. Hopefully, you can empower your UI in your project now with data-binding. 

Pagination is the way to go for fetching items from the API and making infinite scrolling. This helps reduce network usage and improve the performance of Android applications. And that’s it. I hope you can make your application more powerful with pagination. 

RESOURCES

Open Event Codebase: https://github.com/fossasia/open-event-attendee-android/pull/2012

Documentation: https://developer.android.com/topic/libraries/architecture/paging/ 

Google Codelab: https://codelabs.developers.google.com/codelabs/android-paging/#0

Continue Reading

Dialog Component in SUSI.AI

Dialog Component in SUSI.AI is rendered in App.js to remove code redundancy. Redux is integrated in the Dialog component which allows us to open/close the dialog from any component by altering the modal states. This implementation allows us to get rid of the need of having dialog component in different components.

Redux Code

There are two actions and reducers which control the dialog component. Default state of isModalOpen is false and modalType is an empty string. To open a dialog modal the action openModal is dispatched, which sets isModalOpen to true and the modalType. To close a dialog modal the action closeModal is dispatched, which sets isModalOpen to default state i.e. false.

import { handleActions } from 'redux-actions';
import actionTypes from '../actionTypes';

const defaultState = {
 modalProps: {
   isModalOpen: false,
   modalType: '',
 },
};

export default handleActions(
 {
   [actionTypes.UI_OPEN_MODAL](state, { payload }) {
     return {
       ...state,
       modalProps: {
         isModalOpen: true,
         ...payload,
       },
     };
   },
   [actionTypes.UI_CLOSE_MODAL](state) {
     return {
       ...state,
       modalProps: defaultState.modalProps,
     };
   },
 }
 defaultState,
);

Shared Dialog Component

Dialog Modal can be opened from any component by dispatching an action. 

To open a Dialog Modal: this.props.actions.openModal({modalType: [modal name]});

To close a Dialog Modal: this.props.actions.closeModal();

Shared Dialog Component has a DialogData object which contains objects with two main properties : Dialog component and Dialog size. Other props can also be passed along with these two properties such as fullScreen. Dialog Content of different Dialogs are present in their respective folders. Each Dialog Content has a Title, Content and Actions.Different Dialog types present are:

  1. Confirm Delete with Input: This dialog modal is used when a user deletes account, device and skill. 
  2. Confirm Dialog: This dialog modal is used where confirmation is required from the user/admin such as on changing skill status, on password reset,etc.
  3. Share Dialog: This dialog modal opens up when the share icon is clicked in the chat.
  4. Standard Action Dialog: This dialog modal opens up on restore skill, delete feedback, system settings and bot.
  5. Tour Dialog: This dialog modal opens up SUSI.AI tour.

To add a new Dialog to DialogSection, the steps are:

  1. Import the Dialog Content Component
  2. Add the Dialog Component to DialogData object in the following manner:
const DialogData = {
[dialog componet name]: { Component : [imported dialog component name], size : [size of the Dialog Component]},
}

Code (Reduced)

const DialogData = {
  login: { Component: Login, size: 'sm' },
}
const DialogSection = props => {
 const {
   actions,
   modalProps: { isModalOpen, modalType, ...otherProps },
   visited,
 } = props;

 const getDialog = () => {
   if (isModalOpen) {
     return DialogData[modalType];
   }
   return DialogData.noComponent;
 };

 const { size, Component, fullScreen = false } = getDialog();

return (
   <Dialog
      maxWidth={size}
      fullWidth={true}
      open={isModalOpen || !visited}
      onClose={isModalOpen ? actions.closeModal : actions.setVisited}
      fullScreen={fullScreen}
    >
     <DialogContainer>
       {Component ? <Component {...otherProps} /> : null}
     </DialogContainer>
  </Dialog>
)
};

In conclusion, having a shared dialog component reduces redundant code and allows to have a similar Dialog UI across the repo. Also having one component managing all the dialogs removes the possibility of  two dialogs being fired up at once.

Resources

Continue Reading

My Devices in SUSI.AI

In this blog I’ll be explaining how to view, edit and delete connected devices from SUSI.AI webclient. To connect a device open up the SUSI.AI android app, and fill the details accordingly. Device can also be connected by logging in to your raspberry pi. Once the devices is connected you can edit, delete and access specific features for the device from the web client.

My Devices

All the connected devices can be viewed in My Devices tab in the Dashboard. In this tab all the devices connected to your account are listed in a table along with their locations on the map. Each device table row has three action buttons – view, edit and delete. Clicking on the view button takes to device specific page. Clicking on the edit button makes the fields name and room editable in table row. Clicking on the delete button opens a confirm with input dialog. Device can be deleted by entering the device name and clicking on delete.

To fetch all the device getUserDevices action is dispatched on component mounting which sets the reducer state devices in settings reducer. initialiseDevices function is called after all the devices are fetched from the server. This function creates an array of objects of devices with name, room, macId, latitude, longitude and location.

 componentDidMount() {
   const { accessToken, actions } = this.props;
   if (accessToken) {
     actions
       .getUserDevices()
       .then(({ payload }) => {
         this.initialiseDevices();
         this.setState({
           loading: false,
           emptyText: 'You do not have any devices connected yet!',
         });
       })
       .catch(error => {
         this.setState({
           loading: false,
           emptyText: 'Some error occurred while fetching the devices!',
         });
         console.log(error);
       });
   }
   document.title =
     'My Devices - SUSI.AI - Open Source Artificial Intelligence for Personal  Assistants, Robots, Help Desks and Chatbots';
 }
 initialiseDevices = () => {
   const { devices } = this.props;
 
   if (devices) {
     let devicesData = [];
     let deviceIds = Object.keys(devices);
     let invalidLocationDevices = 0;
 
     deviceIds.forEach(eachDevice => {
       const {
         name,
         room,
         geolocation: { latitude, longitude },
       } = devices[eachDevice];
 
       let deviceObj = {
         macId: eachDevice,
         deviceName: name,
         room,
         latitude,
         longitude,
         location: `${latitude}, ${longitude}`,
       };
 
       if (
         deviceObj.latitude === 'Latitude not available.' ||
         deviceObj.longitude === 'Longitude not available.'
       ) {
         deviceObj.location = 'Not found';
         invalidLocationDevices++;
       } else {
         deviceObj.latitude = parseFloat(latitude);
         deviceObj.longitude = parseFloat(longitude);
       }
       devicesData.push(deviceObj);
     });
 
     this.setState({
       devicesData,
       invalidLocationDevices,
     });
   }
 };

Device Page

Clicking on the view icon button in my devices redirects to mydevices/:macId. This page consists of device information in tabular format, local configuration settings and location of the device on the map. User can edit and delete the device from actions present in table. Local configuration settings can be accessed only if the user is logged in the local server.

Edit Device

To edit a device click on the edit icon button in the actions column of the table. The name and room field become editable.On changing the values handleChange function is called which updates the devicesData state. Clicking on the tick icon saves the new details by calling the onDeviceSave function. This function class the addUserDevice api which takes in the new device details.

startEditing = rowIndex => {
   this.setState({ editIdx: rowIndex });
 };
 
 handleChange = (e, fieldName, rowIndex) => {
   const value = e.target.value;
   let data = this.state.devicesData;
   this.setState({
     devicesData: data.map((row, index) =>
       index === rowIndex ? { ...row, [fieldName]: value } : row,
     ),
   });
 };

 handleDeviceSave = rowIndex => {
   this.setState({
     editIdx: -1,
   });
   const deviceData = this.state.devicesData[rowIndex];
 
   addUserDevice({ ...deviceData })
     .then(payload => {})
     .catch(error => {
       console.log(error);
     });
 };

Delete Device

To delete a device click on the delete icon button under the actions column in the table. Clicking on the delete device button opens up the confirm with input dialog modal. Type in the name of the device and click on delete. Clicking on delete calls the handeRemoveDevice function which calls the removeUserDevice api which takes in the macId. On deleting the device user is redirected to the My Devices in Dashboard.

 handleRemoveConfirmation = () => {
   this.props.actions.openModal({
     modalType: 'deleteDevice',
     name: this.state.devicesData[0].deviceName,
     handleConfirm: this.handleRemoveDevice,
     handleClose: this.props.actions.closeModal,
   });
 };
 handleRemoveDevice = () => {
   const macId = this.macId;
   removeUserDevice({ macId })
     .then(payload => {
       this.props.actions.closeModal();
       window.location.replace('/mydevices);
     })
     .catch(error => {
       console.log(error);
     });
 };

In conclusion, My Devices tab in dashboard helps you manage the devices connected with your account along with specific device configuration. Now the users can edit, view and delete their connected devices.

Resources

Continue Reading

Tax Information on Public Ticket Page

This blog post will elaborate on how Tax Information is being displayed on the public page of an event. In current implementation, the user gets to know the total tax inclusive amount only after he/she decides to place an order but no such information was given to them on the public ticket page itself.

Order summary example in eventyay

Example : In initial implementation, the user gets to know that the order is of only $120 and no information is given about the additional 30% being charged and taking the total to $156.

To tackle this issue, I added two hybrid components to the ticket object to handle the two tax cases : 

  • Inclusion in the price : In European and Asian Countries , the tax amount is included in the ticket price itself. For this case, I created the following parameter to store the tax amount included in gross amount.
// app/models/ticket.js
includedTaxAmount: computed('event.tax.isTaxIncludedInPrice', 'event.tax.rate', function() {
  const taxType = this.event.get('tax.isTaxIncludedInPrice');
  if (taxType) {
    const taxRate = this.event.get('tax.rate');
    return ((taxRate * this.price) / (100 + taxRate)).toFixed(2);
  }
  return 0;
})
  • Added on the ticket price : In basic US tax policy, the tax amount is added on top of the ticket price. For such cases I have added a new attribute to ticket model which calculates the total amount payable for that particular ticket with tax inclusion
// app/models/ticket.js
ticketPriceWithTax: computed('event.tax.isTaxIncludedInPrice', 'event.tax.rate', function() {
  let taxType = this.event.get('tax.isTaxIncludedInPrice');
  if (!taxType) {
    return ((1 + this.event.get('tax.rate') / 100) * this.price).toFixed(2);
  }
  return this.price;
})

Now, the public ticket page has to be edited accordingly. The design I decided to follow is inspired by eventbrite itself : 

Eventbrite specimen of the proposed implementation

For this implementation, I modified the ticket list template to accommodate the changes in the following way : 

// app/components/public/ticket-list.hbs
<
td id="{{ticket.id}}_price">
{{currency-symbol eventCurrency}} {{format-number ticket.price}}
{{#
if (and taxInfo (not-eq ticket.type 'free'))}}
  {{#
if showTaxIncludedMessage}}
    <
small class="ui gray-text small">
      {{t 'includes'}} {{currency-symbol eventCurrency}} {{format-number ticket.includedTaxAmount}}
    </
small>
  {{else}}
    <
small class="ui gray-text small">
      + {{currency-symbol eventCurrency}} {{format-number (sub ticket.ticketPriceWithTax ticket.price)}}
    </
small>
  {{/
if}}
  <
div>
    <
small class="ui gray-text tiny aligned right">({{taxInfo.name}})</small>
  </
div>
{{/
if}}
</
td>
Tax amount is included in ticket price

Hence making the new public ticket list display to look like this in case of tax amount inclusion and additional charge as follows

Tax amount is charged over the base price

Discount Code application cases:

In the cases when a user applies the discount code, the ticket price need to be updated, hence, the tax applied has to be updated accordingly. I achieved this by updating the two computed properties of the ticket model on each togglePromotionalCode and applyPromotionalCode action. When a promotional code is applied, the appropriate attribute is updated according to the discount offered.

// app/components/public/ticket-list.js
tickets.forEach(ticket => {
let ticketPrice = ticket.get('price');
let taxRate = ticket.get('event.tax.rate');
let discount = discountType === 'amount' ? Math.min(ticketPrice, discountValue) : ticketPrice * (discountValue / 100);
ticket.set('discount', discount);
if (taxRate && !this.showTaxIncludedMessage) {
  let ticketPriceWithTax = (ticketPrice - ticket.discount) * (1 + taxRate / 100);
  ticket.set('ticketPriceWithTax', ticketPriceWithTax);
} else if (taxRate && this.showTaxIncludedMessage) {
  let includedTaxAmount = (taxRate * (ticketPrice - discount)) / (100 + taxRate);
  ticket.set('includedTaxAmount', includedTaxAmount);
}
this.discountedTickets.addObject(ticket);

Similarly, on toggling the discount code off, the ticket’s computed properties are set back to their initial value using the same formula kept during the time of initialization which has been achieved in the following manner.

// app/components/public/ticket-list.js
this.discountedTickets.forEach(ticket => {
let taxRate = ticket.get('event.tax.rate');
let ticketPrice = ticket.get('price');
if (taxRate && !this.showTaxIncludedMessage) {
  let ticketPriceWithTax = ticketPrice * (1 + taxRate / 100);
  ticket.set('ticketPriceWithTax', ticketPriceWithTax);
} else if (taxRate && this.showTaxIncludedMessage) {
  let includedTaxAmount = (taxRate * ticketPrice) / (100 + taxRate);
  ticket.set('includedTaxAmount', includedTaxAmount);
}
ticket.set('discount', 0);
});

This particular change makes sure that the tax amount is calculated properly as per the discounted amount and thus eliminates the possibility of overcharging the attendee.

Tax recalculation for discounted tickets

In conclusion, this feature has been implemented keeping in mind the consumer’s interest in using the Open Event Frontend and the ease of tax application on the public level with minimum required network requests.

Resources

Related Work and Code Repository

Continue Reading

Data Binding with Kotlin in Eventyay Attendee

Databinding is a common and powerful technique in Android Development. Eventyay Attendee has found many situations where data binding comes in as a great solution for our complex UI. Let’s take a look at this technique.

  • Problems without data binding in Android Development
  • Implementing Databinding with Kotlin inside Fragment
  • Implementing Databinding with Kotlin inside RecyclerView/Adapter
  • Results and GIF
  • Conclusions

PROBLEMS WITHOUT DATABINDING IN ANDROID DEVELOPMENT

Getting the data and fetching it to the UI is a basic work in any kind of application. With Android Development, the most common way to do is it to call function like .setText(), isVisible = True/False,.. in your fragment. This can create many long boilerplate codes inside Android classes. Databinding removes them and moves to the UI classes (XML).

IMPLEMENTING DATABINDING IN FRAGMENT VIEW

Step 1: Enabling data binding in the project build.gradle

android {
   dataBinding {
       enabled = true
   }

Step 2: Wrap the current layout with <layout></layout> tag. Inside that, put <data></data> to indicate any variables needed for data binding. For example, this code here display an event variable for our fragment about event details:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:bind="http://schemas.android.com/tools">

   <data>

       <variable
           name="event"
           type="org.fossasia.openevent.general.event.Event" />
   </data>

   <androidx.coordinatorlayout.widget.CoordinatorLayout
       android:id="@+id/eventCoordinatorLayout"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:background="@android:color/white">

Step 3: Bind your data in the XML file and create a Binding Adapter class for better usage

With the setup above, you can start binding your data with “@{<data code here>}”

<TextView
   android:id="@+id/eventName"
   android:layout_width="0dp"
   android:layout_height="wrap_content"
   android:layout_marginLeft="@dimen/layout_margin_large"
   android:layout_marginTop="@dimen/layout_margin_large"
   android:layout_marginRight="@dimen/layout_margin_large"
   android:text="@{event.name}"
   android:fontFamily="sans-serif-light"
   android:textColor="@color/dark_grey"
   android:textSize="@dimen/text_size_extra_large"
   app:layout_constraintEnd_toEndOf="@+id/eventImage"
   app:layout_constraintStart_toStartOf="@+id/eventImage"
   app:layout_constraintTop_toBottomOf="@+id/eventImage"
   tools:text="Open Source Meetup" />

Sometimes, to bind our data normally we need to use a complex function, then creating Binding Adapter class really helps. For example, Eventyay Attendee heavily uses Picasso function to fetch image to ImageView:

@BindingAdapter("eventImage")
fun setEventImage(imageView: ImageView, url: String?) {
   Picasso.get()
       .load(url)
       .placeholder(R.drawable.header)
       .into(imageView)
}
<ImageView
   android:id="@+id/eventImage"
   android:layout_width="@dimen/layout_margin_none"
   android:layout_height="@dimen/layout_margin_none"
   android:scaleType="centerCrop"
   android:transitionName="eventDetailImage"
   app:layout_constraintDimensionRatio="2"
   app:layout_constraintEnd_toEndOf="parent"
   app:layout_constraintHorizontal_bias="0.5"
   app:layout_constraintStart_toStartOf="parent"
   app:eventImage="@{event.originalImageUrl}"
   app:layout_constraintTop_toBottomOf="@id/alreadyRegisteredLayout" />

Step 4: Finalize data binding setup in Android classes. We can create a binding variable. The binding root will serve as the root node of the layout. Whenever data is needed to be bind, set the data variable stated to that binding variable and call function executePendingBingdings()

private lateinit var rootView: View
private lateinit var binding: FragmentEventBinding
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_event, container, false)
rootView = binding.root
binding.event = event
binding.executePendingBindings()

SOME NOTES

  • In the example mentioned above, the name of the binding variable class is auto-generated based on the name of XML file + “Binding”. For example, the XML name was fragment_event so the DataBinding classes generated name is FragmentEventBinding.
  • The data binding class is only generated only after compiling the project.
  • Sometimes, compiling the project fails because of some problems due to data binding without any clear log messages, then that’s probably because of error when binding your data in XML class. For example, we encounter a problem when changing the value in Attendee data class from firstname to firstName but XML doesn’t follow the update. So make sure you bind your data correctly
<TextView
   android:id="@+id/name"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_marginBottom="@dimen/layout_margin_large"
   android:textColor="@color/black"
   android:textSize="@dimen/text_size_expanded_title_large"
   android:text="@{attendee.firstname + ` ` + attendee.lastname}"
   tools:text="@string/name_preview" />

CONCLUSION

Databinding is the way to go when working with a complex UI in Android Development. This helps reducing boilerplate code and to increase the readability of the code and the performance of the UI. One problem with data binding is that sometimes, it is pretty hard to debug with an unhelpful log message. Hopefully, you can empower your UI in your project now with data binding.  

RESOURCES

Eventyay Attendee Android Codebase: https://github.com/fossasia/open-event-android

Eventyay Attendee Android PR: #1961 – feat: Set up data binding for Recycler/Adapter

Documentation: https://developer.android.com/topic/libraries/data-binding

Google Codelab: https://codelabs.developers.google.com/codelabs/android-databinding/#0

Continue Reading

How to fix undetected Arduino boards in Android

In the development process of the Neurolab Android app, we needed an Arduino-Android connection. This blog explains how to  establish the connection and getting the Arduino board detected in my Android device

Context-connecting the board and getting it detected

Arduino boards are primarily programmed from the Desktop using the Arduino IDE, but they are not limited to the former. Android devices can be used to program the circuit boards using an application named Arduinodroid.

Arduino is basically a platform for building various types of electronic projects and the best part about it is that, it is open-sourced. Arduino, the company has got two products The physical programmable circuit board (often referred to as a microcontroller). 

Examples of Arduino circuit boards – UNO, UNO CH340G, Mega, etc. Find more here.

Connecting the board and getting it detected

Arduino boards are primarily programmed from the Desktop using the Arduino IDE, but they are not limited to the former. Android devices can be used to program the circuit boards using an application named Arduinodroid.

In this blog, we are going to use Arduinodroid app for establishing a connection between the Arduino board and the Android device, getting the board detected in the Android phone and uploading a sketch to it.

Materials/Gadgets required:-

  1. Arduino board (UNO preferably)
  2. Arduino-USB Cable
  3. OTG Cable
  4. Android device

Now, one of the most frequent issues, while establishing a connection and getting the Arduino board detected with the Android device, is the error message of: “No Arduino boards detected” in the Arduinodroid app. There can be a few core reasons for this –

  1. Your Android mobile device isn’t USB-OTG supported – Probably because it is an old model or it might be a company/brand-specific issue.
  2. Disabled OTG Mode – Be sure to enable USB-OTG mode (only if your device has one) from the Developer options in your Android device settings.

Even after trying and making sure of these above points, if you still continue to get an error while uploading a sketch from the Arduinodroid app like this:

                                                            Figure 1: The Error Message

Follow the steps below carefully and simultaneously one after the other:

  1. Look for any external module attached to your Arduino board using jumper wires. If so, remove those connections completely and press the reset button on the Arduino circuit board. The attached modules can be one of the following: Micro SD Card module, Bluetooth module, etc.
  2. Remove pin connections, if any from the TX and RX pin-slots in the Arduino board. These pre-attached pins can cause unnecessary signal transfers which can hinder and make the actual port of Arduino board busy.
  3. Before connecting the Arduino to the Android device, go to the drop down menu in the app at the top-right corner -> Settings -> Board Type -> Arduino -> UNO
  4. Now, you need to code a sketch and make it ready for compile and upload to the circuit board. We will use a basic example sketch for this case. Feel free to try out your own custom coded Arduino sketches. Go to the drop-down menu -> Sketch -> Examples -> Basics -> AnalogReadSignal
  5. Don’t compile the sketch yet because we haven’t connected any Arduino circuit board to our Android device. So first, connect the Arduino circuit board to the Android device through the OTG cable connected to the Arduino-USB cable.
  6. You should see some LEDs lit up on the circuit board (indicates power is flowing to the board). Go ahead to compile the sketch. Click the ‘lightning’ icon on the top in the toolbar of the app. You should see the code/sketch getting compiled. Once done you should see a toast message saying “Compilation finished”. This signifies that your code/sketch has been verified by the compiler.

                                              Figure 2: Successful Compilation of sketch

This process is inevitable and there is hardly any issue while compiling a sketch.

       7. Upload the sketch: Click on the upload icon from the toolbar in the app. Upload             should start once you get a pop-up dialog like this:

                                           Figure 3: Arduino board detected successfully

Once you click Okay, the upload shall start and if your code is correct and matches the particular Arduino circuit board, you shall get a successful upload, which was not the case earlier for the error : “no Arduino boards found” on clicking the upload button.

So, that’s it then. Hope this blog adds value to your development skills and you can continue working bug free with your Android-Arduino connections.

Resources:

  1. Author – Nick Gamon, Article – Have I bricked my Arduino uno problems with uploading to board, Date – Nov’16 2016, Website – https://arduino.stackexchange.com/questions/13292/have-i-bricked-my-arduino-uno-problems-with-uploading-to-board
  2. Author – Arduino Products, Article – Arduino boards, Website – https://www.arduino.cc/en/Main/Boards

3. Author – Anton Smirnov, App name – ArduinoDroid, Website – https://play.google.com/store/apps/details?id=name.antonsmirnov.android.arduinodroid2&hl=en_IN

Tags: FOSSASIA, Neurolab, GSOC19, Open-source, Arduino, Serial terminal

Continue Reading

Implementing Attendee Forms in Wizard of Open Event Frontend

This blog post illustrates on how the order form is included in the attendee information of the Open Event Frontend form  and enabling the organizer to choosing what information to collect from the attendee apart from the mandatory data i.e. First Name, Last Name and the Email Id during the creation of event itself.

The addition of this feature required alteration in the existing wizard flow to accommodate this extra step. This new wizard flow contains the step :

  • Basic Details : Where organizer fills the basic details regarding the event.
  • Attendee Form : In this step, the organizer can choose what information he/she has to collect from the ticket buyers.
  • Sponsors : This step enables the organizer to fill in the sponsor details
  • Session and Speakers : As the name suggests, this final step enables the organizer to fill in session details to be undertaken during the event.

This essentially condensed the flow to this :

The updated wizard checklist

To implement this, the navigation needed to be altered first in the way that Forward and Previous buttons comply to the status bar steps

// app/controller/create.jsmove() {
    this.saveEventDataAndRedirectTo(
      'events.view.edit.attendee',
      ['tickets', 'socialLinks', 'copyright', 'tax', 'stripeAuthorization']
    );
  }
//app/controller/events/view/edit/sponsorship
move(direction) {
    this.saveEventDataAndRedirectTo(
      direction === 'forwards' ? 'events.view.edit.sessions-speakers' : 'events.view.edit.attendee',
      ['sponsors']
    );
  }

Once the navigation was done, I decided to add the step in the progress bar by simply including the attendees form in the event mixin.

// app/mixins/event-wizard.js
    {
      title     : this.l10n.t('Attendee Form'),
      description : this.l10n.t('Know your audience'),
      icon     : 'list icon',
      route     : 'events.view.edit.attendee'
    }

Now a basic layout for the wizard is prepared, all what is left is setting up the route for this step and including it in the router file. I took my inspiration for setting up the route from events/view/tickets/order-from.js and implemented it like this:

// app/routes/events/view/edit/attendee.js
import Route from '@ember/routing/route';
import CustomFormMixin from 'open-event-frontend/mixins/event-wizard';
import { A } from '@ember/array';
export default Route.extend(CustomFormMixin, {

titleToken() {
  return this.l10n.t('Attendee Form');
},

async model() {
  let filterOptions = [{
    name : 'form',
    op : 'eq',
    val : 'attendee'
  }];

  let data = {
    event: this.modelFor('events.view')
  };
  data.customForms = await data.event.query('customForms', {
    filter       : filterOptions,
    sort         : 'id',
    'page[size]' : 50
  });

  return data;
},
afterModel(data) {
  /**
    * Create the additional custom forms if only the compulsory forms exist.
    */
  if (data.customForms.length === 3) {
    let customForms = A();
    for (const customForm of data.customForms ? data.customForms.toArray() : []) {
      customForms.pushObject(customForm);
    }

    const createdCustomForms = this.getCustomAttendeeForm(data.event);

    for (const customForm of createdCustomForms ? createdCustomForms : []) {
      customForms.pushObject(customForm);
    }

    data.customForms = customForms;
  }
}
});

With the route setup and included in the router, I just need to take care of the form data and pass it to the server. Thankfully, the project was already using EventWizardMixin so all I had to do was utilize these functions (save and move) which saves the event data in the status user decides to save it in i.e. either published or draft state

// app/controllers/events/view/edit/attendee.js
import Controller from '@ember/controller';
import EventWizardMixin from 'open-event-frontend/mixins/event-wizard';

export default Controller.extend(EventWizardMixin, {
async saveForms(data) {
  for (const customForm of data.customForms ? data.customForms.toArray() : []) {
    await customForm.save();
  }
  return data;
},
actions: {
  async save(data) {
    try {
      await this.saveForms(data);
      this.saveEventDataAndRedirectTo(
        'events.view.index',
        []
      );
    } catch (error) {
      this.notify.error(this.l10n.t(error.message));
    }
  },
  async move(direction, data) {
    try {
      await this.saveForms(data);
      this.saveEventDataAndRedirectTo(
        direction === 'forwards' ? 'events.view.edit.sponsors' : 'events.view.edit.basic-details',
        []
      );
    } catch (error) {
      this.notify.error(this.l10n.t(error.message));
    }
  }
}
});

Apart from that, the form design was already there, essentially, I reutilized the form design provided to an event organizer / co-organizer in the ticket section of the event dashboard to make it look like this form :

Basic attendee information collection

In the end, after utilizing the existing template and adding it in the route’s template, the implementation is ready for a test run!

// app/templates/events/view/edit/attendee.hbs
{{forms/wizard/attendee-step data=model move='move' save='save' isLoading=isLoading}}

This is a simple test run of how the attendees form step works as others work fine along with it!

Demonstration of new event submission workflow

Resources

Related Work and Code Repository

Continue Reading

Dependency Injection with Kotlin Koin in Eventyay Attendee

Eventyay Attendee Android app contains a lot of shared components between classes that should be reused. Dependency Injection with Koin really comes in as a great problem solver.

Dependency Injection is a common design pattern used in various projects, especially with Android Development. In short, dependency injection helps to create/provide instances to the dependent class, and share it among other classes.

  • Why using Koin?
  • Process of setting up Koin in the application
  • Results
  • Conclusion
  • Resources

Let’s get into the details

WHY USING KOIN?

Before Koin, dependency injection in Android Development was mainly used with other support libraries like Dagger or Guice. Koin is a lightweight alternative that was developed for Kotlin developers. Here are some of the major things that Koin can do for your project:

  • Modularizing your project by declaring modules
  • Injecting class instances into Android classes
  • Injecting class instance by the constructor
  • Supporting with Android Architecture Component and Kotlin
  • Testing easily

SETTING UP KOIN IN THE ANDROID APPLICATION

Adding the dependencies to build.gradle

// Koin
implementation "org.koin:koin-android:$koin_version"
implementation "org.koin:koin-androidx-scope:$koin_version"
implementation "org.koin:koin-androidx-viewmodel:$koin_version"

Create a folder to manage all the dependent classes.

Inside this Modules class, we define modules and create “dependency” class instances/singletons that can be reused or injected. For Eventyay Attendee, we define 5 modules: commonModule, apiModule, viewModelModule, networkModule, databaseModule. This saves a lot of time as we can make changes like adding/removing/editing the dependency in one place.

Let’s take a look at what is inside some of the modules:

DatabaseModule

val databaseModule = module {

   single {
       Room.databaseBuilder(androidApplication(),
           OpenEventDatabase::class.java, "open_event_database")
           .fallbackToDestructiveMigration()
           .build()
   }

   factory {
       val database: OpenEventDatabase = get()
       database.eventDao()
   }

   factory {
       val database: OpenEventDatabase = get()
       database.sessionDao()
   }

CommonModule

val commonModule = module {
   single { Preference() }
   single { Network() }
   single { Resource() }
   factory { MutableConnectionLiveData() }
   factory<LocationService> { LocationServiceImpl(androidContext()) }
}

ApiModule

val apiModule = module {
   single {
       val retrofit: Retrofit = get()
       retrofit.create(EventApi::class.java)
   }
   single {
       val retrofit: Retrofit = get()
       retrofit.create(AuthApi::class.java)
   }

NetworkModule

single {
   val connectTimeout = 15 // 15s
   val readTimeout = 15 // 15s

   val builder = OkHttpClient().newBuilder()
       .connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS)
       .readTimeout(readTimeout.toLong(), TimeUnit.SECONDS)
       .addInterceptor(HostSelectionInterceptor(get()))
       .addInterceptor(RequestAuthenticator(get()))
       .addNetworkInterceptor(StethoInterceptor())

   if (BuildConfig.DEBUG) {
       val httpLoggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
       builder.addInterceptor(httpLoggingInterceptor)
   }
   builder.build()
}

single {
   val baseUrl = BuildConfig.DEFAULT_BASE_URL
   val objectMapper: ObjectMapper = get()
   val onlineApiResourceConverter = ResourceConverter(
       objectMapper, Event::class.java, User::class.java,
       SignUp::class.java, Ticket::class.java, SocialLink::class.java, EventId::class.java,
       EventTopic::class.java, Attendee::class.java, TicketId::class.java, Order::class.java,
       AttendeeId::class.java, Charge::class.java, Paypal::class.java, ConfirmOrder::class.java,
       CustomForm::class.java, EventLocation::class.java, EventType::class.java,
       EventSubTopic::class.java, Feedback::class.java, Speaker::class.java, FavoriteEvent::class.java,
       Session::class.java, SessionType::class.java, MicroLocation::class.java, SpeakersCall::class.java,
       Sponsor::class.java, EventFAQ::class.java, Notification::class.java, Track::class.java,
       DiscountCode::class.java, Settings::class.java, Proposal::class.java)

   Retrofit.Builder()
       .client(get())
       .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
       .addConverterFactory(JSONAPIConverterFactory(onlineApiResourceConverter))
       .addConverterFactory(JacksonConverterFactory.create(objectMapper))
       .baseUrl(baseUrl)
       .build()
}

As described in the code, Koin support single for creating a singleton object, factory for creating a new instance every time an object is injected.

With all the modules created, it is really simple to get Koin running in the project with the function startKoin() and a few lines of code. We use it inside the application class:

startKoin {
   androidLogger()
   androidContext([email protected])
   modules(listOf(
       commonModule,
       apiModule,
       viewModelModule,
       networkModule,
       databaseModule
   ))
}

Injecting created instances defined in the modules can be used in two way, directly inside a constructor or injecting into Android classes.  

Here is an example of dependency injection to the constructor that we used for a ViewModel class and injecting that ViewModel class into the Fragment:

class EventsViewModel(
   private val eventService: EventService,
   private val preference: Preference,
   private val resource: Resource,
   private val mutableConnectionLiveData: MutableConnectionLiveData,
   private val config: PagedList.Config,
   private val authHolder: AuthHolder
) : ViewModel() {
class EventsFragment : Fragment(), BottomIconDoubleClick {
   private val eventsViewModel by viewModel<EventsViewModel>()
   private val startupViewModel by viewModel<StartupViewModel>()

For testing, it is also really easy with support library from Koin.

@Test
fun testDependencies() {
   koinApplication {
       androidContext(mock(Application::class.java))
       modules(listOf(commonModule, apiModule, databaseModule, networkModule, viewModelModule))
   }.checkModules()
}

CONCLUSION

Koin is really easy to use and integrate into Kotlin Android project. Apart from some of the basic functionalities mention above, Koin also supports other helpful features like Scoping or Logging with well-written documentation and examples. Even though it is only developed a short time ago, Koin has proved to be a great use in the Android community. So the more complicated your project is, the more likely it is that dependency injection with Koin will be a good idea.

RESOURCES 

Documentation: https://insert-koin.io/

Eventyay Attendee Android Codebase: https://github.com/fossasia/open-event-android

Continue Reading

Integrating Redux with SUSI.AI Web Clients

In this blog post, we are going to go through the implementation of the Redux integration on the SUSI.AI web clients. The existing SUSI.AI WebChat codebase has Flux integrated into it, but integrating Redux would make it a lot easier to manage the app state in a single store. And would result in a more maintainable and performant application. Let us go through the implementation in the blog –

The key steps involved the following –

  • Restructuring the directory structure of the repository to enable better maintenance.
  • Creating a Redux store and configuring the middlewares.
  • Standardizing the format for writing actions and make API calls on dispatching an action.
  • Standardizing the format for writing reducers.
  • Hook the components to the Redux store.

Restructuring the directory structure

DIrectory structure for https://chat.susi.ai
  • All the redux related files and utils are put into the redux directory, to avoid any sort of confusion, better maintenance and enhanced discoverability. The prime reason for it also because the integration was done side-by-side the existing Flux implementation.
  • The actions and reducers directory each has a index.js, which exports all the actions and reducers respectively, so as to maintain a single import path for the components and this also helped to easily split out different types of actions/reducers.

Creating Redux store and configure middlewares

import { createStore as _createStore, applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux';
import reduxPromise from 'redux-promise';
import reducers from './reducers';

export default function createStore(history) {
 // Sync dispatched route actions to the history
 const reduxRouterMiddleware = routerMiddleware(history);
 const middleware = [reduxRouterMiddleware, reduxPromise];

 let finalCreateStore;
 finalCreateStore = applyMiddleware(...middleware)(_createStore);

 const store = finalCreateStore(
   reducers,
   {},
   window.__REDUX_DEVTOOLS_EXTENSION__ &&
     window.__REDUX_DEVTOOLS_EXTENSION__(),
 );

 return store;
}
  • The function createStore takes in the browserHistory (provided by React Router) and returns a single store object that is passed on to the entry point component of the App.
  • The store is passed to the application via the <Provider> component, provided by the react-redux. It is wrapped to the <App> component as follows –

ReactDOM.render(
 <Provider store={store} key="provider">
   <App />
 </Provider>,
 document.getElementById('root'),
);
  • The 2 middlewares used are routerMiddleware provided by the react-router-redux and the reduxPromise provided by redux-promise.
  • The routerMiddleware enhances a history instance to allow it to synchronize any changes it receives into application state.
  • The reduxPromise returns a promise to the caller so that it can wait for the operation to finish before continuing. This is useful to assist the application to tackle async behaviour.

Standardizing the actions and making API calls on action dispatch

  • The actions file contains the following –

import { createAction } from 'redux-actions';
import actionTypes from '../actionTypes';
import * as apis from '../../apis';

const returnArgumentsFn = function(payload) {
 return Promise.resolve(payload);
};

export default {
// API call on action dispatch
 getApiKeys: createAction(actionTypes.APP_GET_API_KEYS, apis.fetchApiKeys),
// Returns a promise for actions not requiring API calls
 logout: createAction(actionTypes.APP_LOGOUT, returnArgumentsFn),
};

  • As new actions are added, it can be added to the actionTypes file and can be added in the export statement of the above snippet. This enables very standard and easy to manage actions.
  • This approach allows to handle both types of action dispatch – with and without API call. In case of API call on dispatch, the action resolves with the payload of the API.
  • The APIs are called via the AJAX helper (Check out this blog – https://blog.fossasia.org/make-a-helper-for-ajax-requests-using-axios/ ).

Standardizing the reducers and combining them

  • The reducers file contains the following –

import { handleActions } from 'redux-actions';
import actionTypes from '../actionTypes';

const defaultState = {
 ...
 apiKeys: {},
 ...
};

export default handleActions({
  [actionTypes.APP_GET_API_KEYS](state, { payload }) {
     const { keys } = payload;
     return {
       ...state,
       apiKeys: { ...keys },
     };
  },
  ...
},defaultState);
  • The default application state is defined and the reducer corresponding to each action type returns an immutable object which is the new application state,
  • In the above example, the payload from the getApiKeys API call is received in the reducer, which then updated the store.
  • The reducers/index.js  combines all the reducers using combineReducers provided by redux and exports a single object of reducers.

import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import app from './app';
import settings from './settings';
import messages from './messages';

export default combineReducers({
 routing: routerReducer,
 app,
 settings,
 messages,
});

Hook the components to the Redux store

  • After Redux integration and standardization of the reducers, actions, any of the component can be hooked to the store as follows –

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

class MyComponent extends Component {
 componentDidMount() {
    // Dispatch an action
    this.props.actions.getApiKeys();
 }
 render() {
   const { apiKeys } = this.props;
   return (
     <div>
        /* JSX */
     </div>     
   );
 }
}

function mapStateToProps(store) {
 const { apiKeys } = store.app;
 return {
   apiKeys
 };
}

function mapDispatchToProps(dispatch) {
 return {
   actions: bindActionCreators(actions, dispatch),
 };
}

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);
  • The mapStateToProps is a function that is used to provide the store data to the component via props, whereas mapDispatchToProps is used to provide the action creators as props to the component.

The above was the implementation of Redux in the SUSI.AI Web Clients. I hope the blog provided a detailed insight of how Redux was integrated and the standards that were followed.

References

Continue Reading

Make a helper for AJAX requests using axios

In this blog post, we are going to go through the implementation of the helper function that is created for making AJAX requests using axios. Currently, the AJAX calls are made in a very random manner where the calls are written in the component itself and involves rewriting of headers, etc for the API call. Let us go through the implementation in the blog, which will standardise the way to make API calls in SUSI.AI web clients.

The primary changes are –

  • Making a common helper for AJAX requests with the help of axios.
  • Making a common file containing all the API calls across the project.

Going through the implementation

The API calls within the repository were not being made in an organised way, also a lot of redundant code was present. The aim of creating the helper is that, all the API calls is called via this common function. It takes care of the headers and also sending access_token with the API if the user is already logged in for API calls requiring authentication. The function for a API request now looks this simple –

// API call for signup
export function getSignup(payload) {
 const { email, password } = payload;
 const url = `${API_URL}/${AUTH_API_PREFIX}/signup.json`;
 return ajax.get(url, { signup: email, password });
}
  • In the above snippet, the ajax is the common helper used for making API calls. ajax is an object of functions that returns a promise for  various methods of API requests
  • We have primarily taken into consideration GET & POST requests type.
  • The helper function is as follows –
/* Insert imports here*/
const cookies = new Cookies();
const obj = {};

['get', 'post', 'all'].forEach(function(method) {
 obj[method] = function(url, payload, settings = {}) {
   /* Request will be aborted after 30 seconds */
   settings = {
     timeout: 30000,
     dataType: 'json',
     crossDomain: true,
     ...settings,
   };
   
   // Check if logged in
   if (cookies.get('loggedIn')) {
     payload = {
       access_token: cookies.get('loggedIn'),
       ...payload,
     };
   }

   return new Promise(function(resolve, reject) {
     let methodArgs = [];
     if (method === 'post') {
       if (payload && payload instanceof FormData !== true) {
           // Convert to Form Data
           payload = toFormData(payload);
       }

       settings.headers = {
         'Content-Type': 'application/x-www-form-urlencoded',
         ...settings.headers,
       };
     } else if (method === 'get') {
       if (payload) {
         // Add params to the URL   
         url += `?${Object.keys(payload)
           .map(key => key + '=' + payload[key])
           .join('&')}`;
       }
     }

     const methodsToAxiosMethodsMap = {
       get: 'get',
       post: 'post',
       all: 'all',
     };

     if (method === 'all') {
       methodArgs = [url];
     } else if (method === 'get') {
       methodArgs = [url, settings];
     } else {
       methodArgs = [url, payload, settings];
     }

     axios[methodsToAxiosMethodsMap[method]].apply({}, methodArgs).then(
       function(data = {}, ...restSuccessArgs) {
         const statusCode = _.get(data, 'status');
         /*  Send only api response */
         let responseData = { statusCode, ..._.get(data, 'data') };

         if (method === 'all') {
           responseData = data;
           responseData.statusCode = statusCode;
         }

         if (payload) {
           responseData.requestPayload = payload;
         }
         // Mark the promise resolved and return the payload
         resolve(camelizeKeys(responseData), ...restSuccessArgs);
       },
       function(data = {}, ...restErrorArgs) {
         // If request is canceled by user
         if (axios.isCancel(data)) {
           reject(data);
         }

         const statusCode = _.get(data, 'response.status', -1);
         let responseData = { statusCode, ..._.get(data, 'response.data') };

         if (method === 'all') {
           responseData = data;
           responseData.statusCode = statusCode;
         }

         if (payload) {
           responseData.requestPayload = payload;
         }
         // Mark the promise rejected and return the payload
         reject(camelizeKeys(responseData), ...restErrorArgs);
       },
     );
   });
 };
});

export default obj;
  • The above objects contains 3 functions –
    • ajax.get(url, payload, settings) – GET – The helper adds the query params to the URL by iterating through them and appending it to the URL and joining them with &.
    • ajax.post(url, payload, settings) –  POST – The helper checks, if the POST requests contains a payload which is an instance of form data, it converts to toFormData payload.
    • ajax.all(url, payload, settings) – ALL – It helps to deal with concurrent requests.
  • The url is the complete API endpoint, payload consists of the data/request payload. The settings consists of any headers related info, that needs to be added exclusively to the axios config.
  • There is also no need to pass access token to each API request as a payload. The helper check whether the user is logged-in and adds the access_token to the request payload. The snippet below demonstrates it –
if (cookies.get('loggedIn')) {
     payload = {
       access_token: cookies.get('loggedIn'),
       ...payload,
     };
   }
  • The access token, if present in the cookies is added to the payload, therefore authenticating the API call.
  • The keys of the response are changed to camel case before being sent to the caller function. It is done to maintain variable nomenclature standards across the app and also to follow javascript guidelines.
  • The file containing the API calls is structured as follows –
import ajax from '../helpers/ajax';
import urls from '../utils/urls';

const { API_URL } = urls;
const AUTH_API_PREFIX = 'aaa';
const CHAT_API_PREFIX = 'susi';
const CMS_API_PREFIX = 'cms';

// API without request payload
export function fetchApiKeys() {
 const url = `${API_URL}/${AUTH_API_PREFIX}/getApiKeys.json`;
 return ajax.get(url);
}

// API with request payload
export function getLogin(payload) {
 const { email, password } = payload;
 const url = `${API_URL}/${AUTH_API_PREFIX}/login.json`;
 return ajax.get(url, { login: email, password, type: 'access-token' });
}

The above was the implementation of the helper function that is created for making AJAX requests using axios. I hope the blog provided a detailed insight of it helps in making the process of making API calls more standardised and easier.

References

Continue Reading
Close Menu