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

Implementing places autosuggestion with Mapbox for searching events in Eventyay Attendee

In Eventyay Attendee, searching for events has always been a core function that we focus on. When searching for events based on location, autosuggestion based on user input really comes out as a great feature to increase the user experience. Let’s take a look at the implementation

  • Why using Mapbox?
  • Integrating places autosuggestion for searching
  • Conclusion
  • Resources

WHY USING MAPBOX?

There are many Map APIs to be taken into consideration but we choose Mapbox as it is really to set up and use, good documentation and reasonable pricing for an open-source project compared to other Map API.

INTEGRATING PLACES AUTOSUGGESTION FOR SEARCHING

Step 1: Setup dependency in the build.gradle + the MAPBOX key

//Mapbox java sdk
implementation ‘com.mapbox.mapboxsdk:mapbox-sdk-services:4.8.0’

Step 2: Set up functions inside ViewModel to handle autosuggestion based on user input:

private fun loadPlaceSuggestions(query: String) {
  // Cancel Previous Call
  geoCodingRequest?.cancelCall()
  doAsync {
      geoCodingRequest = makeGeocodingRequest(query)
      val list = geoCodingRequest?.executeCall()?.body()?.features()
      uiThread { placeSuggestions.value = list }
  }
}

private fun makeGeocodingRequest(query: String) = MapboxGeocoding.builder()
  .accessToken(BuildConfig.MAPBOX_KEY)
  .query(query)
  .languages(“en”)
  .build()

Based on the input, the functions will update the UI with new inputs of auto-suggested location texts. The MAPBOX_KEY can be given from the Mapbox API.

Step 3: Create an XML file to display autosuggestion strings item and set up RecyclerView in the main UI fragment

Step 4: Set up ListAdapter and ViewHolder to bind the list of auto-suggested location strings. Here, we use CamenFeature to set up with ListAdapter as the main object. With the function .placeName(), information about the location will be given so that ViewHolder can bind the data

class PlaceSuggestionsAdapter :
  ListAdapter<CarmenFeature,
      PlaceSuggestionViewHolder>(PlaceDiffCallback()) {

  var onSuggestionClick: ((String) -> Unit)? = null

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaceSuggestionViewHolder {
      val itemView = LayoutInflater.from(parent.context)
          .inflate(R.layout.item_place_suggestion, parent, false)
      return PlaceSuggestionViewHolder(itemView)
  }

  override fun onBindViewHolder(holder: PlaceSuggestionViewHolder, position: Int) {
      holder.apply {
          bind(getItem(position))
          onSuggestionClick = [email protected]
      }
  }

  class PlaceDiffCallback : DiffUtil.ItemCallback<CarmenFeature>() {
      override fun areItemsTheSame(oldItem: CarmenFeature, newItem: CarmenFeature): Boolean {
          return oldItem.placeName() == newItem.placeName()
      }

      override fun areContentsTheSame(oldItem: CarmenFeature, newItem: CarmenFeature): Boolean {
          return oldItem.equals(newItem)
      }
  }
}
fun bind(carmenFeature: CarmenFeature) {
  carmenFeature.placeName()?.let {
      val placeDetails = extractPlaceDetails(it)
      itemView.placeName.text = placeDetails.first
      itemView.subPlaceName.text = placeDetails.second
      itemView.subPlaceName.isVisible = placeDetails.second.isNotEmpty()

      itemView.setOnClickListener {
          onSuggestionClick?.invoke(placeDetails.first)
      }
  }
}

Step 5: Set up RecyclerView with Adapter created above:

private fun setupRecyclerPlaceSuggestions() {
  rootView.rvAutoPlaces.layoutManager = LinearLayoutManager(context)
  rootView.rvAutoPlaces.adapter = placeSuggestionsAdapter

  placeSuggestionsAdapter.onSuggestionClick = {
      savePlaceAndRedirectToMain(it)
  }
}

RESULTS

CONCLUSION

Place Autocorrection is a really helpful and interesting feature to include in your next project. With the help of Mapbox SDK, it is really easy to implement to enhance your user experience in your application.

RESOURCES

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

Eventyay Attendee PR: #1594 – feat: Mapbox Autosuggest

Documentation: https://docs.mapbox.com/android/plugins/overview/places/

Continue Reading

Adding Google Analytics To SUSI.AI

Google analytics provides SUSI.AI Admins with a way to analyze traffic and get advanced metrics. Google Analytics first collects data, computes the data, and showcases it on console dashboard. It is used for keeping track of user behavior on the website.

How Google Analytics Work

Below shown are fields used by Google Analytics to get user data. A cookie is stored into user browser. _ga stays in the browser for 2 years, _gid for 2 days.

Whenever a user performs an event like a mouse click, page change, open popup, add query strings to URL, information is sent to Google Analytics using an API call describing the user event. Below is the photo describing it:

The above-bordered boxes consist of information sent by google analytics. The information consists of:

  • The user identification code
  • The device resolution of screen used by a user
  • User language
  • The URL of the page user is on

How it processes data

When a user with tracking code lands on SUSI.AI, Google Analytics creates a unique random identity and attaches it to the user cookie.

Each new user is given a unique ID. Whenever a new ID is detected, analytics considers it as a new user. But when an existing ID is detected, it’s considered as a returning user and set over with the hit.

A unique ID code is fetched from every new unique user. Whenever a new user ID is detected in the call, Google Analytics treats the unique ID as a new user. If the ID matches from earlier ID, the user is a returning user and calculates the metrics another way. Each new user gets a unique ID.

However, a new user is detected if the same user clears out the browser cookie or uses another device over the same IP address to view the webpage.

When an existing ID is detected, it’s considered as a returning user and set over with the hit.

Code Integration

Google Analytics must be initialized using initialize(gaTrackingID) function before any of the other tracking functions will record any data. Using react-ga ga(‘create’, …), the values are sent to google analytics

import withTracker from ‘./withTracker’;
import GoogleAnalytics from ‘react-ga’;
..

actions.getApiKeys().then(({ payload }) => {
  const {
    keys: { googleAnalyticsKey = null },
   } = payload;
   googleAnalyticsKey && GoogleAnalytics.initialize(googleAnalyticsKey);
});
..
<Route exact path=”/” component={withTracker(BrowseSkill)} />
<Route exact path=”/chat” component={withTracker(ChatApp)} />

src/App.js

Higher Order Component for Tracking page activity

A Higher Order Component is a function that returns an enhanced component by adding some more properties or logic and allows reusing component logic.

Using HOC for tracking page helped by not exposing internal component with tracking. The withTracker HOC wraps all the component, which SUSI.AI wants to track and exposes a trackerPage method. 

  • Whenever a component mounts, we track the new pages using componentDidMount. 
  • Whenever the component updates and location of browser url changes, we track the sub pages using componentDidUpdate lifecycle hook.
import React, { Component } from ‘react’;
import GoogleAnalytics from ‘react-ga’;
import PropTypes from ‘prop-types’;

const withTracker = (WrappedComponent, options = { }) => {
  const trackPage = page => {
    GoogleAnalytics.set({
      page,
      …options,
    });
    GoogleAnalytics.pageview(page);
  };
  const HOC = class extends Component {
    componentDidMount() {
      window.scrollTo(0, 0);
      const page = this.props.location.pathname + this.props.location.search;
      trackPage(page);
    }

    componentDidUpdate(prevProps) {
      const currentPage =
        prevProps.location.pathname + prevProps.location.search;
      const nextPage =
        this.props.location.pathname + this.props.location.search;

      if (currentPage !== nextPage) {
        trackPage(nextPage);
      }
      if (this.props.location.pathname !== prevProps.location.pathname) {
        window.scrollTo(0, 0);
      }
    }

    render() {
      return <WrappedComponent {…this.props} />;
    }
  };

  HOC.propTypes = {
    location: PropTypes.object,
  };

  return HOC;
};

export default withTracker;

src/withTracker.js

The Higher-Order Component pattern turned out to be really useful to achieve D.R.Y (Don’t Repeat Yourself) and keeping component separate from tracking. With React Analytics being added, we can track various metrics, live users on site and see how SUSI.AI traffic is performing over time. 

Resources

Tags

SUSI.AI, FOSSASIA, GSoC19, Google Analytics, Higher Order Components

Continue Reading

How to use and implement Save Wave Configs feature in Pocket Science Lab Wave Generator

What is a Wave Generator?

A Wave Generator is one of the most important features of PSLab. It is used to generate different kinds of waves like, sine, triangular, square, PWM. Wave generator UI is as under:

  (Figure 1 : Wave Generator Analog Mode UI)
  (Figure 2 : Wave Generator Digital Mode UI)

As can be seen the Screenshot above user is provided with options to set Frequency, Phase, Duty of different waves and once configurations are set user can either output the waves in Oscilloscope or can compare different waves in Logic Analyzer.

What is Save Wave Configs Feature?


        (Figure 3 : Wave Generator Control Buttons (View,Save,Mode))

In this feature, the user is given a ”Save” button to use this feature. 

The reason to add this feature is that, sometimes we need to perform the same experiment multiple times, is such scenarios if we have to set wave configurations everytime, it will become boring and there will be chances of errors. Hence using the save configs feature, user can currently set configurations in the Local Storage and can use it anytime later. 

Further since the Wave Configurations are saved on Local Storage as .CSV file, a user can save configs and can share the file with others so others can as well set their device to same configurations. The saved Wave Configurations can be seen in the DataLogger Activity and opening a saved log would take the user to Wave Generator Activity where all the configs will be set as per the saved log.

A sample CSV of the log data can be seen below.


(Figure 4: Wave Configs CSV file)

How is Save Configs Feature Implemented

The implementation of this feature is quite simple. There is a class named WaveData.  With the parameters of Mode(Square or PWM), Wave name, Shape, Freq, Phase and Duty. Whenever the user clicks the save configs button, the saveWaveConfigs()  function is called. This function fetches set values of different fields and creates realm objects and also write them to csv file as shown above. Once the realm objects are created, this log can be seen in the Data Logger Activity. The code to generate the realm object for the wave configs (that is the implementation of the function saveWaveConfig()) is given below.

public void saveWaveConfig(View view) {
        long block = System.currentTimeMillis();
        csvLogger.prepareLogFile();
          csvLogger.writeMetaData(getResources().getString(R.string.wave_generator));
        long timestamp;
        double lat, lon;
        String data = "Timestamp,DateTime,Mode,Wave,Shape,Freq,Phase,Duty,lat,lon\n";
        recordSensorDataBlockID(new SensorDataBlock(block, getResources().getString(R.string.wave_generator)));

So till now in the function, we create a header string for the data to be stored in the csv file. We create a block from the current system time. This block will be used to save all the realm object for this function, so all the objects created at this instance will be grouped as a single log entry in DataLoggerActivity.

double freq1 = (double) (WaveGeneratorCommon.wave.get(WaveConst.WAVE1).get(WaveConst.FREQUENCY));
double freq2 = (double) WaveGeneratorCommon.wave.get(WaveConst.WAVE2).get(WaveConst.FREQUENCY);
double phase = (double) WaveGeneratorCommon.wave.get(WaveConst.WAVE2).get(WaveConst.PHASE);

String waveType1 = WaveGeneratorCommon.wave.get(WaveConst.WAVE1).get(WaveConst.WAVETYPE) == SIN ? "sine" : "tria";
String waveType2 = WaveGeneratorCommon.wave.get(WaveConst.WAVE2).get(WaveConst.WAVETYPE) == SIN ? "sine" : "tria";

timestamp = System.currentTimeMillis();
String timeData = timestamp + "," + CSVLogger.FILE_NAME_FORMAT.format(new Date(timestamp));
String locationData = lat + "," + lon;

Next, in the function we get currently set Frequency for both analog waves and phase in the variables. We also store the selected wave shape for each of the waves. Since each entry in the csv file is required to have a timestamp and a location stamp,here we create common stamps of both types and will append it to each entry further in the function. 

if (scienceLab.isConnected()) {
            if (digital_mode == WaveConst.SQUARE) {
                data += timeData + ",Square,Wave1," + waveType1 + "," + String.valueOf(freq1) + ",0,0," + locationData + "\n"; //wave1
                recordSensorData(new WaveGeneratorData(timestamp, block, "Square", "Wave1", waveType1, String.valueOf(freq1), "0", "0", lat, lon));
                data += timeData + ",Square,Wave2," + waveType2 + "," + String.valueOf(freq2) + "," + String.valueOf(phase) + ",0," + locationData + "\n";//wave2
                recordSensorData(new WaveGeneratorData(timestamp + 1, block, "Square", "Wave2", waveType2, String.valueOf(freq2), String.valueOf(phase), "0", lat, lon));

Here we check whether the currently selected mode is Analog(Square) or Digital (PWM). Above code snippet is for the SQUARE mode block. We create WaveGeneratorData object for both SI1 and SI2 waves based on the parameters we stored earlier. We also append the data to a string, data.  Which we will later use to write the log into a csv file.

else {
   double freqSqr1 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR1).get(WaveConst.FREQUENCY);
   double dutySqr1 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR1).get(WaveConst.DUTY) / 100;
   double dutySqr2 = ((double) WaveGeneratorCommon.wave.get(WaveConst.SQR2).get(WaveConst.DUTY)) / 100;
   double phaseSqr2 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR2).get(WaveConst.PHASE) / 360;
   double dutySqr3 = ((double) WaveGeneratorCommon.wave.get(WaveConst.SQR3).get(WaveConst.DUTY)) / 100;
   double phaseSqr3 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR3).get(WaveConst.PHASE) / 360;
   double dutySqr4 = ((double) WaveGeneratorCommon.wave.get(WaveConst.SQR4).get(WaveConst.DUTY)) / 100;
   double phaseSqr4 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR4).get(WaveConst.PHASE) / 360;

 data += timeData + ",PWM,Sq1,PWM," + String.valueOf(freqSqr1) + ",0," + String.valueOf(dutySqr1) + "," + locationData + "\n";

 recordSensorData(new WaveGeneratorData(timestamp, block, "PWM", "Sq1", "PWM", String.valueOf(freqSqr1), "0", String.valueOf(dutySqr1), lat, lon));
}

The above code snippet shows a block of the condition when the selected mode is PWM. Here we store the set values of Freq, Phase and Duty for each SQ1, SQ2, SQ3 and SQ4 waves into variables. Once we store the values we create WaveGeneratorData objects for each of the waves and also append the data to the data string to write to the csv. The code above includes details only for SQ1, but exact same procedure is followed for SQ2, SQ3, and SQ4. One we have all the data appended to the string we call the following function to write the data to csv file. 

 csvLogger.writeCSVFile(data);

We can see that this function basically stores the current set values of different params into a WaveData object. For each of the waveforms in selected mode (analog/digital), a new instance of WaveData object is created and stored into realm.

When the user opens one of the logs, setReceivedData() function is called in WaveGeneratorActivity. This function iterates on the received realm objects and based on the attributes of each object the data is set in the UI automatically. The implementation of this function is given below, 

public void setReceivedData() {
        for (WaveGeneratorData data : recordedWaveData) {
            Log.d("data", data.toString());
            if (data.getMode().equals(MODE_SQUARE)) {
                WaveGeneratorCommon.mode_selected = WaveConst.SQUARE;
                switch (data.getWave()) {
                    case "Wave1":
                        if (data.getShape().equals("sine")) {
                            WaveGeneratorCommon.wave.get(WaveConst.WAVE1).put(WaveConst.WAVETYPE, SIN);
                        } else {
                            WaveGeneratorCommon.wave.get(WaveConst.WAVE1).put(WaveConst.WAVETYPE, TRIANGULAR);
                        }
                        WaveGeneratorCommon.wave.get(WaveConst.WAVE1).put(WaveConst.FREQUENCY, Double.valueOf(data.getFreq()).intValue());
                        break;
                }
                enableInitialState();
            } 

This function iterates over the received WaveGeneratorData objects. For each object we check what is the mode of the waveData. The above code snippet is used when the mode is SQUARE. We get the waveType from the object, and since for SQUARE mode there are only 2 types : Wave1 and Wave2, we set the attributes for each wave as we get them from the objects using WaveGeneratorCommon

else if (data.getMode().equals(MODE_PWM)) {
                WaveGeneratorCommon.mode_selected = WaveConst.PWM;
                switch (data.getWave()) {
                    case "Sq1":
                        WaveGeneratorCommon.wave.get(WaveConst.SQR1).put(WaveConst.FREQUENCY, Double.valueOf(data.getFreq()).intValue());
                        WaveGeneratorCommon.wave.get(WaveConst.SQR1).put(WaveConst.DUTY, ((Double) (Double.valueOf(data.getDuty()) * 100)).intValue());
                        break;
                }
                enableInitialStatePWM();
            }
        }

Same as before if the mode of the object is PWM, there will be 4 cases : SQ1, SQ2, SQ3 and SQ4. And depending on the data stored in the received objects.

In a nutshell this features enables to save and reuse wave configuration with ease. 

A small video to explain the whole functionality of this feature can be found here. 

References

Write to a file in Android

Code Repository

PSLab Android

Tags

PSLab, Wave Generator, SaveConfig, Android, GSoC 19

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

Implementation of Donation Tickets in Open Event

Implementation of donation tickets in Open Event Project

This blog post explains the implementation details of donation tickets in the Open Event Project (Eventyay). Eventyay is the Open Event management solution which allows users to buy & sell tickets, organize events & promote their brand. This was developed at FOSSASIA. 

Prior to the integration of this feature, the organizer had the option to provide only paid and free tickets. These tickets had a fixed price and therefore, imbibing and integrating this into the system was relatively easier. The biggest challenge in the implementation of donation tickets was variable prices. The subtotal, total and validation checks had to updated dynamically and shouldn’t be breaking any of the previous features.  

The organizer requires an option to add donation tickets when they create/edit an event by specifying the appropriate minimum price, maximum price and quantity.

                         Organizer View – Donation Tickets

To integrate these features pertaining to donation tickets, fields for minimum and maximum prices had to be introduced into the tickets model. The maximum & minimum prices for free and paid tickets would be the same as the normal price but it’s variant for donations.

isDonationPriceValid: computed('[email protected]', '[email protected]', function() {

for (const donationTicket of this.donationTickets) { if (donationTicket.orderQuantity > 0) {

if (donationTicket.price < donationTicket.minPrice || donationTicket.price > donationTicket.maxPrice) { return false;

}

}

}

return true;

})

Check for valid donation price

To validate the minimum and maximum prices, ember validations have been implemented which checks whether the min price is lesser than or equal to the max  price to ensure a proper flow. Also, in addition to front-end validations, server side checks have also been implemented to ensure that incorrect data does now propagate through the server.

In addition to that, these checks also had to be integrated in the pre-existing shouldDisableOrderButton computed property. Therefore, if the order has invalid donation tickets, the order button would be disabled.

For the public event page, the donation tickets segment have a section which specifies the price range in which the price must lie in. If the user enters a price out of the valid range, a validation error occurs.

The way in which these validation rules have been implemented was the biggest challenge in this feature as multiple sets of donation tickets might be present. As each set of donation tickets have a different price range, these validation rules had to be generated dynamically using semantic ui validations


donationTicketsValidation: computed('[email protected]', '[email protected]', '[email protected]', function() {

const validationRules = {};

for (let donationTicket of this.donationTickets) { validationRules[donationTicket.id] = {

identifier : donationTicket.id,

optional : true,

rules :

[

{

type : `integer[${donationTicket.minPrice}..${donationTicket.maxPrice}]`,

prompt : this.l10n.t(`Please enter a donation amount between ${donationTicket.minPrice} and ${donationTicket.maxPrice}`)

}

]

};

}

return validationRules;

})

Dynamic validation rule generation for donation tickets

Each donation ticket had to be looped through to add a validation rule corresponding to the donation ticket’s ID. These rules were then returned from a computed property.

Resources:

Related work and code repo:

Tags:

Eventyay, FOSSASIA, Flask, SQLAlchemy, Open Event, Python, JWT

Continue Reading

Implementing Slideshow Servlet in SUSI.AI Skills

Slideshow shown on SUSI.AI homepage helps SUSI.AI client showcase interesting and new features integrated into the platform. It helps to display the capabilities of SUSI.AI and other interesting areas. The slideshow can be configured from the Admin panel of SUSI.AI. 

For storing slideshow data, images, information, redirect to link on slideshow click, we need to implement a servlet to store data on server-side.

The endpoint is of GET type, and accepts:

  • redirect_link(compulsory): redirect link if a user clicks on the slider image
  • image_name(compulsory): The image relative folder path on the server
  • info: Any relevant information about the slider
  • deleteSlide: True, if the user wants to delete slider

Code Integration

For implementing slideshow service, we need to store the image on the backend using uploadImage service and using the uploaded image file path in the backend to store the full slider details using skillSlideshowService service. 

SkillSlideshowService:

For setting the slideshow, the minimum permission required is ADMIN

@Override
   public UserRole getMinimalUserRole() {
       return UserRole.ADMIN;
   }

   @Override
   public JSONObject getDefaultPermissions(UserRole   baseUserRole) {
       return null;
   }

   @Override
   public String getAPIPath() {
       return “/cms/skillSlideshow.json”;
   }

cms/SkillSlideshowService.java

Let’s have a look at how it is implemented, the redirect_link and image_name are necessary parameters and if not passed throws exception. If appropriate parameters are present, get the user query data using query.call. Access the data on the server side through DAO.skillSlideshow, if slideshow key is present in JsonTray skillSlideshow, get JSONObject with key “slideshow”.

If deleteKey is false, create a new JSONObject and put the query call data inside it and add to skillSlideshow object with redirectUrl as the key.

If deleteKey is true, remove the object associated with redirect_link and create a new object and add.

public ServiceResponse serviceImpl(Query call, HttpServletResponse response, Authorization authorization,
           final JsonObjectWithDefault permissions) throws APIException {
       if (call.get(“redirect_link”, null) == null || call.get(“image_name”, null) == null) {
           throw new APIException(400, “Bad Request. No enough parameter present”);
       }

       String redirectLink = call.get(“redirect_link”, null);
       String imageName = call.get(“image_name”, null);
       String info = call.get(“info”, null);
       boolean deleteSlide = call.get(“deleteSlide”, false);
       JsonTray skillSlideshow = DAO.skillSlideshow;
       JSONObject result = new JSONObject();
       JSONObject skillSlideshowObj = new JSONObject();
       if (skillSlideshow.has(“slideshow”)) {
           skillSlideshowObj = skillSlideshow.getJSONObject(“slideshow”);
       }
       if (!deleteSlide) {
           try {
               JSONObject slideObj = new JSONObject();
               slideObj.put(“image_name”, imageName);
               slideObj.put(“info”, info);
               skillSlideshowObj.put(redirectLink, slideObj);
               skillSlideshow.put(“slideshow”, skillSlideshowObj, true);
               result.put(“accepted”, true);
               result.put(“message”, “Added new Slide ” + call.get(“redirect_link”) + ” successfully !”);
               return new ServiceResponse(result);
           } catch (Exception e) {
               throw new APIException(500,
                       “Failed : Unable to add slide with path ” + call.get(“redirect_link”) + ” !”);
           }
       } else {
           try {
               skillSlideshowObj.remove(redirectLink);
               skillSlideshow.put(“slideshow”, skillSlideshowObj, true);
               result.put(“accepted”, true);
               result.put(“message”, “Removed Slide with path ” + call.get(“redirect_link”) + ” successfully !”);
               return new ServiceResponse(result);
           } catch (Exception e) {
               throw new APIException(501,
                       “Failed to remove Slide: ” + call.get(“redirect_link”) + ” doesn’t exists!”);
           }
       }
   }

cms/SkillSlideshowService.java

GetSkillSlideshow 

For fetching the slideshow data on frontend, GetSkillSlideshow servlet is implemented. The minimum userRole required in ANONYMOUS.

 @Override
   public String getAPIPath() {
       return “/cms/getSkillSlideshow.json”;
   }

   @Override
   public UserRole getMinimalUserRole() {
       return UserRole.ANONYMOUS;
   }

   @Override
   public JSONObject getDefaultPermissions(UserRole baseUserRole) {
       return null;
   }

cms/GetSkillSlideshow.java

For fetching the slider data, access DAO.skillSlideshow, and get the JSONObject associated with key slideshow and put it in result response, put accepted key as true and return the response

public ServiceResponse serviceImpl(Query call, HttpServletResponse response, Authorization rights, final JsonObjectWithDefault permissions) throws APIException {
       JsonTray skillSlideshow = DAO.skillSlideshow;
       JSONObject skillSlideshowObj = skillSlideshow.getJSONObject(“slideshow”);
       JSONObject result = new JSONObject();
       try {
           result.put(“accepted”, true);
           result.put(“slideshow”, skillSlideshowObj);
           result.put(“message”, “Success : Fetched all Skills Slides!”);
           return new ServiceResponse(result);
       } catch (Exception e) {
           throw new APIException(500, “Failed : Unable to fetch Skills Slides!”);
       }
   }

cms/GetSkillSlideshow.java

3 types of endpoints are required for achieving the slider slideshow functionality. First, when a user creates or edits a slider, the user first needs to upload the image on the server using uploadImage.json service.

Image Suffix is the suffix of the file name stored in SUSI.AI server, susi_icon is the suffix in the image shown below.

Once the image is uploaded on the server, the API returns the relative path to the server location. The path on the server gets filled in ImagePath field on client-side(disabled to users).

With GetSkillSlideshow and SkillSlideshowService implemented, the admins can now manage and control the slideshow shown on the SUSI.AI home page, directly from the admin panel. The client can now easily discover new, exciting features as well.

Resources

Tags

SUSI.AI, FOSSASIA, GSoC`19, SUSI.AI Server

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
Close Menu